1 /*
2  * FileUserPreferences.java 18 sept 2006
3  *
4  * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.io;
21 
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.BufferedInputStream;
25 import java.io.File;
26 import java.io.FileFilter;
27 import java.io.FileInputStream;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.math.BigDecimal;
33 import java.net.MalformedURLException;
34 import java.net.URISyntaxException;
35 import java.net.URL;
36 import java.net.URLClassLoader;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.Iterator;
43 import java.util.LinkedHashSet;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Map.Entry;
48 import java.util.MissingResourceException;
49 import java.util.Properties;
50 import java.util.ResourceBundle;
51 import java.util.Set;
52 import java.util.TreeSet;
53 import java.util.WeakHashMap;
54 import java.util.concurrent.Executor;
55 import java.util.concurrent.Executors;
56 import java.util.prefs.AbstractPreferences;
57 import java.util.prefs.BackingStoreException;
58 import java.util.prefs.Preferences;
59 import java.util.zip.ZipEntry;
60 import java.util.zip.ZipInputStream;
61 
62 import com.eteks.sweethome3d.model.CatalogDoorOrWindow;
63 import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
64 import com.eteks.sweethome3d.model.CatalogTexture;
65 import com.eteks.sweethome3d.model.Content;
66 import com.eteks.sweethome3d.model.FurnitureCatalog;
67 import com.eteks.sweethome3d.model.FurnitureCategory;
68 import com.eteks.sweethome3d.model.LengthUnit;
69 import com.eteks.sweethome3d.model.Library;
70 import com.eteks.sweethome3d.model.PatternsCatalog;
71 import com.eteks.sweethome3d.model.RecorderException;
72 import com.eteks.sweethome3d.model.Sash;
73 import com.eteks.sweethome3d.model.TextureImage;
74 import com.eteks.sweethome3d.model.TexturesCatalog;
75 import com.eteks.sweethome3d.model.TexturesCategory;
76 import com.eteks.sweethome3d.model.UserPreferences;
77 import com.eteks.sweethome3d.tools.OperatingSystem;
78 import com.eteks.sweethome3d.tools.TemporaryURLContent;
79 import com.eteks.sweethome3d.tools.URLContent;
80 
81 /**
82  * User preferences initialized from
83  * {@link com.eteks.sweethome3d.io.DefaultUserPreferences default user preferences}
84  * and stored in user preferences on local file system.
85  * @author Emmanuel Puybaret
86  */
87 public class FileUserPreferences extends UserPreferences {
88   private static final String LANGUAGE                                  = "language";
89   private static final String UNIT                                      = "unit";
90   private static final String EXTENSIBLE_UNIT                           = "extensibleUnit";
91   private static final String CURRENCY                                  = "currency";
92   private static final String VALUE_ADDED_TAX_ENABLED                   = "valueAddedTaxEnabled";
93   private static final String DEFAULT_VALUE_ADDED_TAX_PERCENTAGE        = "defaultValueAddedTaxPercentage";
94   private static final String FURNITURE_CATALOG_VIEWED_IN_TREE          = "furnitureCatalogViewedInTree";
95   private static final String NAVIGATION_PANEL_VISIBLE                  = "navigationPanelVisible";
96   private static final String AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED = "aerialViewCenteredOnSelectionEnabled";
97   private static final String OBSERVER_CAMERA_SELECTED_AT_CHANGE        = "observerCameraSelectedAtChange";
98   private static final String MAGNETISM_ENABLED                         = "magnetismEnabled";
99   private static final String RULERS_VISIBLE                            = "rulersVisible";
100   private static final String GRID_VISIBLE                              = "gridVisible";
101   private static final String DEFAULT_FONT_NAME                         = "defaultFontName";
102   private static final String FURNITURE_VIEWED_FROM_TOP                 = "furnitureViewedFromTop";
103   private static final String FURNITURE_MODEL_ICON_SIZE                 = "furnitureModelIconSize";
104   private static final String ROOM_FLOOR_COLORED_OR_TEXTURED            = "roomFloorColoredOrTextured";
105   private static final String WALL_PATTERN                              = "wallPattern";
106   private static final String NEW_WALL_PATTERN                          = "newWallPattern";
107   private static final String NEW_WALL_THICKNESS                        = "newWallThickness";
108   private static final String NEW_WALL_HEIGHT                           = "newHomeWallHeight";
109   private static final String NEW_WALL_BASEBOARD_THICKNESS              = "newWallBaseboardThickness";
110   private static final String NEW_WALL_BASEBOARD_HEIGHT                 = "newWallBaseboardHeight";
111   private static final String NEW_ROOM_FLOOR_COLOR                      = "newRoomFloorColor";
112   private static final String NEW_FLOOR_THICKNESS                       = "newFloorThickness";
113   private static final String CHECK_UPDATES_ENABLED                     = "checkUpdatesEnabled";
114   private static final String UPDATES_MINIMUM_DATE                      = "updatesMinimumDate";
115   private static final String AUTO_SAVE_DELAY_FOR_RECOVERY              = "autoSaveDelayForRecovery";
116   private static final String AUTO_COMPLETION_PROPERTY                  = "autoCompletionProperty#";
117   private static final String AUTO_COMPLETION_STRINGS                   = "autoCompletionStrings#";
118   private static final String RECENT_COLORS                             = "recentColors";
119   private static final String RECENT_TEXTURE_NAME                       = "recentTextureName#";
120   private static final String RECENT_TEXTURE_CREATOR                    = "recentTextureCreator#";
121   private static final String RECENT_TEXTURE_IMAGE                      = "recentTextureImage#";
122   private static final String RECENT_TEXTURE_WIDTH                      = "recentTextureWidth#";
123   private static final String RECENT_TEXTURE_HEIGHT                     = "recentTextureHeight#";
124   private static final String RECENT_HOMES                              = "recentHomes#";
125   private static final String IGNORED_ACTION_TIP                        = "ignoredActionTip#";
126 
127   private static final String FURNITURE_NAME                            = "furnitureName#";
128   private static final String FURNITURE_CREATOR                         = "furnitureCreator#";
129   private static final String FURNITURE_CATEGORY                        = "furnitureCategory#";
130   private static final String FURNITURE_ICON                            = "furnitureIcon#";
131   private static final String FURNITURE_MODEL                           = "furnitureModel#";
132   private static final String FURNITURE_WIDTH                           = "furnitureWidth#";
133   private static final String FURNITURE_DEPTH                           = "furnitureDepth#";
134   private static final String FURNITURE_HEIGHT                          = "furnitureHeight#";
135   private static final String FURNITURE_MOVABLE                         = "furnitureMovable#";
136   private static final String FURNITURE_DOOR_OR_WINDOW                  = "furnitureDoorOrWindow#";
137   private static final String FURNITURE_ELEVATION                       = "furnitureElevation#";
138   private static final String FURNITURE_COLOR                           = "furnitureColor#";
139   private static final String FURNITURE_MODEL_SIZE                      = "furnitureModelSize#";
140   private static final String FURNITURE_MODEL_ROTATION                  = "furnitureModelRotation#";
141   private static final String FURNITURE_STAIRCASE_CUT_OUT_SHAPE         = "furnitureStaircaseCutOutShape#";
142   private static final String FURNITURE_BACK_FACE_SHOWN                 = "furnitureBackFaceShown#";
143   private static final String FURNITURE_ICON_YAW                        = "furnitureIconYaw#";
144   private static final String FURNITURE_PROPORTIONAL                    = "furnitureProportional#";
145 
146   private static final String TEXTURE_NAME                              = "textureName#";
147   private static final String TEXTURE_CREATOR                           = "textureCreator#";
148   private static final String TEXTURE_CATEGORY                          = "textureCategory#";
149   private static final String TEXTURE_IMAGE                             = "textureImage#";
150   private static final String TEXTURE_WIDTH                             = "textureWidth#";
151   private static final String TEXTURE_HEIGHT                            = "textureHeight#";
152 
153   private static final String FURNITURE_CONTENT_PREFIX                  = "Furniture-3-";
154   private static final String TEXTURE_CONTENT_PREFIX                    = "Texture-3-";
155 
156   private static final String LANGUAGE_LIBRARIES_PLUGIN_SUB_FOLDER      = "languages";
157   private static final String FURNITURE_LIBRARIES_PLUGIN_SUB_FOLDER     = "furniture";
158   private static final String TEXTURES_LIBRARIES_PLUGIN_SUB_FOLDER      = "textures";
159 
160   private static final PreferencesURLContent MISSING_CONTENT;
161 
162   private final Map<String, Boolean> ignoredActionTips = new HashMap<String, Boolean>();
163   private List<ClassLoader>          resourceClassLoaders;
164   private final File                 preferencesFolder;
165   private final File []              applicationFolders;
166   private Preferences                preferences;
167   private Executor                   catalogsLoader;
168   private Executor                   updater;
169   private List<Library>              libraries;
170 
171   private Map<Content, PreferencesURLContent> copiedContentsCache = new WeakHashMap<Content, PreferencesURLContent>();
172 
173   public static final String PLUGIN_LANGUAGE_LIBRARY_FAMILY = "PluginLanguageLibrary";
174 
175   static {
176     PreferencesURLContent dummyURLContent = null;
177     try {
178       dummyURLContent = new PreferencesURLContent(new URL("file:/missingSweetHome3DContent"));
179     } catch (MalformedURLException ex) {
180     }
181     MISSING_CONTENT = dummyURLContent;
182   }
183 
184   /**
185    * Creates user preferences read from user preferences in file system,
186    * and from resource files.
187    */
FileUserPreferences()188   public FileUserPreferences() {
189     this(null, null);
190   }
191 
192   /**
193    * Creates user preferences stored in the folders given in parameter.
194    * @param preferencesFolder the folder where preferences files are stored
195    *    or <code>null</code> if this folder is the default one.
196    * @param applicationFolders the folders where application private files are stored
197    *    or <code>null</code> if it's the default one. As the first application folder
198    *    is used as the folder where plug-ins files are imported by the user, it should
199    *    have write access otherwise the user won't be able to import them.
200    */
FileUserPreferences(File preferencesFolder, File [] applicationFolders)201   public FileUserPreferences(File preferencesFolder,
202                              File [] applicationFolders) {
203     this(preferencesFolder, applicationFolders, null);
204   }
205 
206   /**
207    * Creates user preferences stored in the folders given in parameter.
208    * @param preferencesFolder the folder where preferences files are stored
209    *    or <code>null</code> if this folder is the default one.
210    * @param applicationFolders  the folders where application private files are stored
211    *    or <code>null</code> if it's the default one. As the first application folder
212    *    is used as the folder where plug-ins files are imported by the user, it should
213    *    have write access otherwise the user won't be able to import them.
214    * @param updater  an executor that will be used to update user preferences for lengthy
215    *    operations. If <code>null</code>, then these operations and
216    *    updates will be executed in the current thread.
217    */
FileUserPreferences(File preferencesFolder, File [] applicationFolders, Executor updater)218   public FileUserPreferences(File preferencesFolder,
219                              File [] applicationFolders,
220                              Executor updater) {
221     this.libraries = new ArrayList<Library>();
222     this.preferencesFolder = preferencesFolder;
223     this.applicationFolders = applicationFolders;
224     Executor defaultExecutor = new Executor() {
225         public void execute(Runnable command) {
226           command.run();
227         }
228       };
229     if (updater == null) {
230       this.catalogsLoader =
231       this.updater = defaultExecutor;
232     } else {
233       this.catalogsLoader = Executors.newSingleThreadExecutor();
234       this.updater = updater;
235     }
236 
237     updateSupportedLanguages();
238 
239     final Preferences preferences;
240     // From version 3.0 use portable preferences
241     PortablePreferences portablePreferences = new PortablePreferences();
242     // If portable preferences storage doesn't exist and default preferences folder is used
243     if (!portablePreferences.exist()
244         && preferencesFolder == null) {
245       // Retrieve preferences from pre version 3.0
246       preferences = getPreferences();
247     } else {
248       preferences = portablePreferences;
249     }
250 
251     String language = preferences.get(LANGUAGE, getLanguage());
252     // Check language is still supported
253     if (!Arrays.asList(getSupportedLanguages()).contains(language)) {
254       language = Locale.ENGLISH.getLanguage();
255     }
256     setLanguage(language);
257 
258     setFurnitureCatalog(new FurnitureCatalog());
259     // Fill default furniture catalog
260     updateFurnitureDefaultCatalog(defaultExecutor, defaultExecutor);
261     // Read additional furniture
262     readModifiableFurnitureCatalog(preferences);
263 
264     setTexturesCatalog(new TexturesCatalog());
265     // Fill default textures catalog
266     updateTexturesDefaultCatalog(defaultExecutor, defaultExecutor);
267     // Read additional textures
268     readModifiableTexturesCatalog(preferences);
269 
270     DefaultUserPreferences defaultPreferences = new DefaultUserPreferences(false, this);
271 
272     // Fill default patterns catalog
273     PatternsCatalog patternsCatalog = defaultPreferences.getPatternsCatalog();
274     setPatternsCatalog(patternsCatalog);
275 
276     // Read other preferences
277     LengthUnit defaultLengthUnit = defaultPreferences.getLengthUnit();
278     try {
279       // EXTENSIBLE_UNIT was added in version 4.0 to store new additional length unit
280       // to avoid breaking program if an older version of FileUserPreferences reads new preferences
281       String extensibleUnit = preferences.get(EXTENSIBLE_UNIT, null);
282       if (extensibleUnit != null) {
283         setUnit(LengthUnit.valueOf(extensibleUnit));
284       } else {
285         setUnit(LengthUnit.valueOf(preferences.get(UNIT, defaultLengthUnit.name())));
286       }
287     } catch (IllegalArgumentException ex) {
288       setUnit(defaultLengthUnit);
289     }
290     setCurrency(preferences.get(CURRENCY, defaultPreferences.getCurrency()));
291     setValueAddedTaxEnabled(preferences.getBoolean(VALUE_ADDED_TAX_ENABLED, defaultPreferences.isValueAddedTaxEnabled()));
292     String percentage = preferences.get(DEFAULT_VALUE_ADDED_TAX_PERCENTAGE, null);
293     BigDecimal valueAddedTaxPercentage = defaultPreferences.getDefaultValueAddedTaxPercentage();
294     if (percentage != null) {
295       try {
296         valueAddedTaxPercentage = new BigDecimal(percentage);
297       } catch (NumberFormatException ex) {
298       }
299     }
300     setDefaultValueAddedTaxPercentage(valueAddedTaxPercentage);
301     setFurnitureCatalogViewedInTree(preferences.getBoolean(FURNITURE_CATALOG_VIEWED_IN_TREE,
302         defaultPreferences.isFurnitureCatalogViewedInTree()));
303     setNavigationPanelVisible(preferences.getBoolean(NAVIGATION_PANEL_VISIBLE,
304         defaultPreferences.isNavigationPanelVisible()));
305     setAerialViewCenteredOnSelectionEnabled(preferences.getBoolean(AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED,
306         defaultPreferences.isAerialViewCenteredOnSelectionEnabled()));
307     setObserverCameraSelectedAtChange(preferences.getBoolean(OBSERVER_CAMERA_SELECTED_AT_CHANGE,
308         defaultPreferences.isObserverCameraSelectedAtChange()));
309     setMagnetismEnabled(preferences.getBoolean(MAGNETISM_ENABLED, true));
310     setRulersVisible(preferences.getBoolean(RULERS_VISIBLE, defaultPreferences.isRulersVisible()));
311     setGridVisible(preferences.getBoolean(GRID_VISIBLE, defaultPreferences.isGridVisible()));
312     setDefaultFontName(preferences.get(DEFAULT_FONT_NAME,  defaultPreferences.getDefaultFontName()));
313     setFurnitureViewedFromTop(preferences.getBoolean(FURNITURE_VIEWED_FROM_TOP,
314         defaultPreferences.isFurnitureViewedFromTop()));
315     setFurnitureModelIconSize(preferences.getInt(FURNITURE_MODEL_ICON_SIZE, defaultPreferences.getFurnitureModelIconSize()));
316     setFloorColoredOrTextured(preferences.getBoolean(ROOM_FLOOR_COLORED_OR_TEXTURED,
317         defaultPreferences.isRoomFloorColoredOrTextured()));
318     try {
319       setWallPattern(patternsCatalog.getPattern(preferences.get(WALL_PATTERN,
320           defaultPreferences.getWallPattern().getName())));
321     } catch (IllegalArgumentException ex) {
322       // Ensure wall pattern always exists even if new patterns are added in future versions
323       setWallPattern(defaultPreferences.getWallPattern());
324     }
325     try {
326       if (defaultPreferences.getNewWallPattern() != null) {
327         setNewWallPattern(patternsCatalog.getPattern(preferences.get(NEW_WALL_PATTERN,
328             defaultPreferences.getNewWallPattern().getName())));
329       }
330     } catch (IllegalArgumentException ex) {
331       // Keep new wall pattern unchanged
332     }
333     setNewWallThickness(preferences.getFloat(NEW_WALL_THICKNESS,
334         defaultPreferences.getNewWallThickness()));
335     setNewWallHeight(preferences.getFloat(NEW_WALL_HEIGHT,
336         defaultPreferences.getNewWallHeight()));
337     setNewWallBaseboardThickness(preferences.getFloat(NEW_WALL_BASEBOARD_THICKNESS,
338         defaultPreferences.getNewWallBaseboardThickness()));
339     setNewWallBaseboardHeight(preferences.getFloat(NEW_WALL_BASEBOARD_HEIGHT,
340         defaultPreferences.getNewWallBaseboardHeight()));
341     String newRoomFloorColor = preferences.get(NEW_ROOM_FLOOR_COLOR, null);
342     if (newRoomFloorColor != null) {
343       setNewRoomFloorColor(Integer.decode(newRoomFloorColor) | 0xFF000000);
344     } else {
345       setNewRoomFloorColor(defaultPreferences.getNewRoomFloorColor());
346     }
347     setNewFloorThickness(preferences.getFloat(NEW_FLOOR_THICKNESS,
348         defaultPreferences.getNewFloorThickness()));
349     setCheckUpdatesEnabled(preferences.getBoolean(CHECK_UPDATES_ENABLED,
350         defaultPreferences.isCheckUpdatesEnabled()));
351     if (preferences.get(UPDATES_MINIMUM_DATE, null) != null) {
352       setUpdatesMinimumDate(preferences.getLong(UPDATES_MINIMUM_DATE, 0));
353     }
354     setAutoSaveDelayForRecovery(preferences.getInt(AUTO_SAVE_DELAY_FOR_RECOVERY,
355         defaultPreferences.getAutoSaveDelayForRecovery()));
356     // Read recent colors list
357     String [] recentColors = preferences.get(RECENT_COLORS, "").split(",");
358     List<Integer> recentColorsList = new ArrayList<Integer>(recentColors.length);
359     for (String color : recentColors) {
360       if (color.length() > 0) {
361         recentColorsList.add(Integer.decode(color) | 0xFF000000);
362       }
363     }
364     setRecentColors(recentColorsList);
365     readRecentTextures(preferences);
366     // Read recent homes list
367     List<String> recentHomes = new ArrayList<String>();
368     for (int i = 1; i <= getRecentHomesMaxCount(); i++) {
369       String recentHome = preferences.get(RECENT_HOMES + i, null);
370       if (recentHome != null) {
371         recentHomes.add(recentHome);
372       }
373     }
374     setRecentHomes(recentHomes);
375     // Read ignored action tips
376     for (int i = 1; ; i++) {
377       String ignoredActionTip = preferences.get(IGNORED_ACTION_TIP + i, "");
378       if (ignoredActionTip.length() == 0) {
379         break;
380       } else {
381         this.ignoredActionTips.put(ignoredActionTip, true);
382       }
383     }
384     // Get default auto completion strings
385     for (String property : defaultPreferences.getAutoCompletedProperties()) {
386       setAutoCompletionStrings(property, defaultPreferences.getAutoCompletionStrings(property));
387     }
388     // Read auto completion strings list
389     for (int i = 1; ; i++) {
390       String autoCompletionProperty = preferences.get(AUTO_COMPLETION_PROPERTY + i, null);
391       String autoCompletionStrings = preferences.get(AUTO_COMPLETION_STRINGS + i, null);
392       if (autoCompletionProperty != null && autoCompletionStrings != null) {
393         setAutoCompletionStrings(autoCompletionProperty, Arrays.asList(autoCompletionStrings.split(",")));
394       } else {
395         break;
396       }
397     }
398 
399     setHomeExamples(defaultPreferences.getHomeExamples());
400 
401     addPropertyChangeListener(Property.LANGUAGE, new PropertyChangeListener() {
402         public void propertyChange(PropertyChangeEvent ev) {
403           // Update catalogs with new default locale
404           updateFurnitureDefaultCatalog(catalogsLoader, FileUserPreferences.this.updater);
405           updateTexturesDefaultCatalog(catalogsLoader, FileUserPreferences.this.updater);
406           updateAutoCompletionStrings();
407           setHomeExamples(new DefaultUserPreferences(false, FileUserPreferences.this).getHomeExamples());
408         }
409       });
410 
411     if (preferences != portablePreferences) {
412       // Switch to portable preferences now that all preferences are read
413       this.preferences = portablePreferences;
414     } else {
415       this.preferences = preferences;
416     }
417   }
418 
419   /**
420    * Updates the default supported languages with languages available in plugin folder.
421    */
updateSupportedLanguages()422   private void updateSupportedLanguages() {
423     removeLibraries(LANGUAGE_LIBRARY_TYPE);
424     List<ClassLoader> resourceClassLoaders = new ArrayList<ClassLoader>();
425     String [] defaultSupportedLanguages = getDefaultSupportedLanguages();
426     Set<String> supportedLanguages = new TreeSet<String>(Arrays.asList(defaultSupportedLanguages));
427 
428     File [] languageLibrariesPluginFolders = getLanguageLibrariesPluginFolders();
429     if (languageLibrariesPluginFolders != null) {
430       for (File languageLibrariesPluginFolder : languageLibrariesPluginFolders) {
431         // Try to load sh3l files from language plugin folder
432         File [] pluginLanguageLibraryFiles = languageLibrariesPluginFolder.listFiles(new FileFilter () {
433           public boolean accept(File pathname) {
434             return pathname.isFile();
435           }
436         });
437 
438         if (pluginLanguageLibraryFiles != null) {
439           // Treat language files in reverse order so file named with a date or a version
440           // will be taken into account from most recent to least recent
441           Arrays.sort(pluginLanguageLibraryFiles, Collections.reverseOrder(OperatingSystem.getFileVersionComparator()));
442           for (File pluginLanguageLibraryFile : pluginLanguageLibraryFiles) {
443             try {
444               Set<String> languages = getLanguages(pluginLanguageLibraryFile);
445               if (!languages.isEmpty()) {
446                 supportedLanguages.addAll(languages);
447                 URL pluginFurnitureCatalogUrl = pluginLanguageLibraryFile.toURI().toURL();
448                 URLClassLoader classLoader = new URLClassLoader(new URL [] {pluginFurnitureCatalogUrl});
449                 resourceClassLoaders.add(classLoader);
450 
451                 DefaultLibrary languageLibrary;
452                 try {
453                   languageLibrary = new DefaultLibrary(pluginLanguageLibraryFile.getCanonicalPath(), LANGUAGE_LIBRARY_TYPE,
454                       ResourceBundle.getBundle(PLUGIN_LANGUAGE_LIBRARY_FAMILY, Locale.getDefault(), classLoader));
455                 } catch (MissingResourceException ex) {
456                   languageLibrary = new DefaultLibrary(pluginLanguageLibraryFile.getCanonicalPath(), LANGUAGE_LIBRARY_TYPE,
457                       null, getLanguageLibraryDefaultName(languages), null, getDefaultVersion(pluginLanguageLibraryFile), null, null);
458                 }
459                 libraries.add(0, languageLibrary);
460               }
461             } catch (IOException ex) {
462               // Ignore malformed files
463             }
464           }
465         }
466       }
467     }
468 
469     // Give less priority to default class loader
470     resourceClassLoaders.addAll(super.getResourceClassLoaders());
471     this.resourceClassLoaders = Collections.unmodifiableList(resourceClassLoaders);
472     if (defaultSupportedLanguages.length < supportedLanguages.size()) {
473       setSupportedLanguages(supportedLanguages.toArray(new String [supportedLanguages.size()]));
474     }
475   }
476 
477   /**
478    * Returns the languages included in the given language library file.
479    */
getLanguages(File languageLibraryFile)480   private Set<String> getLanguages(File languageLibraryFile) throws IOException {
481     Set<String> languages = new LinkedHashSet<String>();
482     ZipInputStream zipIn = null;
483     try {
484       // Search if zip file contains some *_xx.properties or *_xx_xx.properties files
485       zipIn = new ZipInputStream(new FileInputStream(languageLibraryFile));
486       for (ZipEntry entry; (entry = zipIn.getNextEntry()) != null; ) {
487         String zipEntryName = entry.getName();
488         int underscoreIndex = zipEntryName.indexOf('_');
489         if (underscoreIndex != -1) {
490           int extensionIndex = zipEntryName.lastIndexOf(".properties");
491           if (extensionIndex != -1 && underscoreIndex < extensionIndex - 2) {
492             String language = zipEntryName.substring(underscoreIndex + 1, extensionIndex);
493             int countrySeparator = language.indexOf('_');
494             if (countrySeparator == 2
495                 && language.length() == 5) {
496               languages.add(language);
497             } else if (language.length() == 2) {
498               languages.add(language);
499             }
500           }
501         }
502       }
503       return languages;
504     } finally {
505       if (zipIn != null) {
506         zipIn.close();
507       }
508     }
509   }
510 
511   /**
512    * Returns a text in English describing the given languages.
513    */
getLanguageLibraryDefaultName(Set<String> languages)514   private String getLanguageLibraryDefaultName(Set<String> languages) {
515     String description = "";
516     for (String language : languages) {
517       if (description.length() > 0) {
518         description += ", ";
519       }
520       int underscoreIndex = language.indexOf('_');
521       Locale locale = underscoreIndex < 0
522           ? new Locale(language)
523           : new Locale(language.substring(0, underscoreIndex), language.substring(underscoreIndex + 1));
524       description += locale.getDisplayLanguage(Locale.ENGLISH);
525       if (underscoreIndex >= 0) {
526         description += " (" + locale.getDisplayCountry(Locale.ENGLISH) + ")";
527       }
528     }
529     if (languages.size() > 1) {
530       description += " languages support";
531     } else {
532       description += " language support";
533     }
534     return description;
535   }
536 
537   /**
538    * Returns a version number from the given file name or <code>null</code>.
539    */
getDefaultVersion(File pluginLanguageLibraryFile)540   private String getDefaultVersion(File pluginLanguageLibraryFile) {
541     String fileName = pluginLanguageLibraryFile.getName();
542     // Search version number between last hyphen and last point
543     int hyphenIndex = fileName.lastIndexOf('-');
544     if (hyphenIndex > 0) {
545       int pointIndex = fileName.lastIndexOf('.');
546       if (pointIndex < 0) {
547         pointIndex = fileName.length();
548       }
549       String version = fileName.substring(hyphenIndex + 1, pointIndex);
550       if (version.matches("[\\d\\.]+")) {
551         return version;
552       }
553     }
554     return null;
555   }
556 
557   /**
558    * Returns the default class loader of user preferences and the class loaders that
559    * give access to resources in language libraries plugin folder.
560    */
561   @Override
getResourceClassLoaders()562   public List<ClassLoader> getResourceClassLoaders() {
563     return this.resourceClassLoaders;
564   }
565 
566   /**
567    * Reloads furniture default catalogs.
568    */
updateFurnitureDefaultCatalog(Executor furnitureCatalogLoader, final Executor updater)569   private void updateFurnitureDefaultCatalog(Executor furnitureCatalogLoader,
570                                              final Executor updater) {
571     final FurnitureCatalog furnitureCatalog = getFurnitureCatalog();
572     furnitureCatalogLoader.execute(new Runnable() {
573         public void run() {
574           updater.execute(new Runnable() {
575               public void run() {
576                 // Delete default furniture of current furniture catalog
577                 for (FurnitureCategory category : furnitureCatalog.getCategories()) {
578                   for (CatalogPieceOfFurniture piece : category.getFurniture()) {
579                     if (!piece.isModifiable()) {
580                       furnitureCatalog.delete(piece);
581                     }
582                   }
583                 }
584               }
585             });
586 
587           // Read default furniture catalog
588           final FurnitureCatalog resourceFurnitureCatalog =
589               readFurnitureCatalogFromResource(getFurnitureLibrariesPluginFolders());
590           for (final FurnitureCategory category : resourceFurnitureCatalog.getCategories()) {
591             for (final CatalogPieceOfFurniture piece : category.getFurniture()) {
592               updater.execute(new Runnable() {
593                   public void run() {
594                     furnitureCatalog.add(category, piece);
595                   }
596                 });
597             }
598           }
599           if (resourceFurnitureCatalog instanceof DefaultFurnitureCatalog) {
600             updater.execute(new Runnable() {
601                 public void run() {
602                   removeLibraries(FURNITURE_LIBRARY_TYPE);
603                   libraries.addAll(((DefaultFurnitureCatalog)resourceFurnitureCatalog).getLibraries());
604                 }
605               });
606           }
607         }
608       });
609   }
610 
611   /**
612    * Returns the furniture catalog contained in resources of the application and in the given plug-in folders.
613    * Caution : This method can be called from constructor so overriding implementations
614    * shouldn't be based on the state of their fields.
615    */
readFurnitureCatalogFromResource(File [] furniturePluginFolders)616   protected FurnitureCatalog readFurnitureCatalogFromResource(File [] furniturePluginFolders) {
617     return new DefaultFurnitureCatalog(this, furniturePluginFolders);
618   }
619 
620   /**
621    * Removes from the list of libraries the ones of the given type.
622    */
removeLibraries(String libraryType)623   private void removeLibraries(String libraryType) {
624     for (Iterator<Library> it = this.libraries.iterator(); it.hasNext(); ) {
625       Library library = it.next();
626       if (library.getType() == libraryType) {
627         it.remove();
628       }
629     }
630   }
631 
632   /**
633    * Reloads textures default catalog.
634    */
updateTexturesDefaultCatalog(Executor texturesCatalogLoader, final Executor updater)635   private void updateTexturesDefaultCatalog(Executor texturesCatalogLoader,
636                                             final Executor updater) {
637     final TexturesCatalog texturesCatalog = getTexturesCatalog();
638     texturesCatalogLoader.execute(new Runnable() {
639         public void run() {
640           updater.execute(new Runnable() {
641               public void run() {
642                 // Delete default textures of current textures catalog
643                 for (TexturesCategory category : texturesCatalog.getCategories()) {
644                   for (CatalogTexture texture : category.getTextures()) {
645                     if (!texture.isModifiable()) {
646                       texturesCatalog.delete(texture);
647                     }
648                   }
649                 }
650               }
651             });
652 
653           // Read default textures catalog
654           final TexturesCatalog resourceTexturesCatalog =
655               readTexturesCatalogFromResource(getTexturesLibrariesPluginFolders());
656           for (final TexturesCategory category : resourceTexturesCatalog.getCategories()) {
657             for (final CatalogTexture texture : category.getTextures()) {
658               updater.execute(new Runnable() {
659                   public void run() {
660                     texturesCatalog.add(category, texture);
661                   }
662                 });
663             }
664           }
665           if (resourceTexturesCatalog instanceof DefaultTexturesCatalog) {
666             updater.execute(new Runnable() {
667                 public void run() {
668                   removeLibraries(TEXTURES_LIBRARY_TYPE);
669                   libraries.addAll(((DefaultTexturesCatalog)resourceTexturesCatalog).getLibraries());
670                 }
671               });
672           }
673         }
674       });
675   }
676 
677   /**
678    * Returns the textures catalog contained in resources of the application and in the given plug-in folders.
679    * Caution : This method can be called from constructor so overriding implementations
680    * shouldn't be based on the state of their fields.
681    */
readTexturesCatalogFromResource(File [] texturesPluginFolders)682   protected TexturesCatalog readTexturesCatalogFromResource(File [] texturesPluginFolders) {
683     return new DefaultTexturesCatalog(this, texturesPluginFolders);
684   }
685 
686   /**
687    * Adds to auto completion strings the default strings of the new chosen language.
688    */
updateAutoCompletionStrings()689   private void updateAutoCompletionStrings() {
690     DefaultUserPreferences defaultPreferences = new DefaultUserPreferences(false, this);
691     for (String property : defaultPreferences.getAutoCompletedProperties()) {
692       for (String autoCompletionString : defaultPreferences.getAutoCompletionStrings(property)) {
693         addAutoCompletionString(property, autoCompletionString);
694       }
695     }
696   }
697 
698   /**
699    * Read recent textures from preferences.
700    */
readRecentTextures(Preferences preferences)701   private void readRecentTextures(Preferences preferences) {
702     File preferencesFolder;
703     try {
704       preferencesFolder = getPreferencesFolder();
705     } catch (IOException ex) {
706       return;
707     }
708     List<TextureImage> recentTextures = new ArrayList<TextureImage>();
709     for (int index = 1; true; index++) {
710       String textureName = preferences.get(RECENT_TEXTURE_NAME + index, null);
711       if (textureName == null) {
712         break;
713       } else {
714         Content image = getContent(preferences, RECENT_TEXTURE_IMAGE + index, preferencesFolder);
715         if (image != MISSING_CONTENT) {
716           float width = preferences.getFloat(RECENT_TEXTURE_WIDTH + index, -1);
717           float height = preferences.getFloat(RECENT_TEXTURE_HEIGHT + index, -1);
718           String creator = preferences.get(RECENT_TEXTURE_CREATOR + index, null);
719           recentTextures.add(new CatalogTexture(null, textureName, image, width, height, creator));
720         }
721       }
722     }
723     setRecentTextures(recentTextures);
724   }
725 
726   /**
727    * Read modifiable furniture catalog from preferences.
728    */
readModifiableFurnitureCatalog(Preferences preferences)729   private void readModifiableFurnitureCatalog(Preferences preferences) {
730     File preferencesFolder;
731     try {
732       preferencesFolder = getPreferencesFolder();
733     } catch (IOException ex) {
734       ex.printStackTrace();
735       return;
736     }
737     CatalogPieceOfFurniture piece;
738     for (int i = 1; (piece = readModifiablePieceOfFurniture(preferences, i, preferencesFolder)) != null; i++) {
739       if (piece.getIcon() != MISSING_CONTENT
740           && piece.getModel() != MISSING_CONTENT) {
741         FurnitureCategory pieceCategory = readModifiableFurnitureCategory(preferences, i);
742         getFurnitureCatalog().add(pieceCategory, piece);
743       }
744     }
745   }
746 
747   /**
748    * Returns the modifiable piece of furniture read from <code>preferences</code> at the given <code>index</code>.
749    * Caution : This method can be called from constructor so overriding implementations
750    * shouldn't be based on the state of their fields.
751    * @param preferences        the preferences from which piece of furniture data can be read
752    * @param index              the index of the read piece
753    * @param preferencesFolder  the folder where piece resources can be stored
754    * @return the read piece of furniture or <code>null</code> if the piece at the given index doesn't exist.
755    */
readModifiablePieceOfFurniture(Preferences preferences, int index, File preferencesFolder)756   protected CatalogPieceOfFurniture readModifiablePieceOfFurniture(Preferences preferences,
757                                                                    int index,
758                                                                    File preferencesFolder) {
759     String name = preferences.get(FURNITURE_NAME + index, null);
760     if (name == null) {
761       // Return null if key furnitureName# doesn't exist
762       return null;
763     }
764     URLContent icon  = getContent(preferences, FURNITURE_ICON + index, preferencesFolder);
765     URLContent model = getContent(preferences, FURNITURE_MODEL + index, preferencesFolder);
766     float width = preferences.getFloat(FURNITURE_WIDTH + index, 0.1f);
767     float depth = preferences.getFloat(FURNITURE_DEPTH + index, 0.1f);
768     float height = preferences.getFloat(FURNITURE_HEIGHT + index, 0.1f);
769     float elevation = preferences.getFloat(FURNITURE_ELEVATION + index, 0);
770     boolean movable = preferences.getBoolean(FURNITURE_MOVABLE + index, false);
771     boolean doorOrWindow = preferences.getBoolean(FURNITURE_DOOR_OR_WINDOW + index, false);
772     String staircaseCutOutShape = preferences.get(FURNITURE_STAIRCASE_CUT_OUT_SHAPE + index, null);
773     String colorString = preferences.get(FURNITURE_COLOR + index, null);
774     Integer color = colorString != null
775         ? Integer.valueOf(colorString) : null;
776     float [][] modelRotation = getModelRotation(preferences, FURNITURE_MODEL_ROTATION + index);
777     boolean backFaceShown = preferences.getBoolean(FURNITURE_BACK_FACE_SHOWN + index, false);
778     String modelSizeString = preferences.get(FURNITURE_MODEL_SIZE + index, null);
779     Long modelSize = modelSizeString != null
780         ? Long.valueOf(modelSizeString) : model.getSize();
781     String creator = preferences.get(FURNITURE_CREATOR + index, null);
782     float iconYaw = preferences.getFloat(FURNITURE_ICON_YAW + index, 0);
783     boolean proportional = preferences.getBoolean(FURNITURE_PROPORTIONAL + index, true);
784 
785     if (doorOrWindow) {
786       return new CatalogDoorOrWindow(name, icon, model,
787           width, depth, height, elevation, movable, 1, 0, new Sash [0],
788           color, modelRotation, backFaceShown, modelSize, creator, iconYaw, proportional);
789     } else {
790       return new CatalogPieceOfFurniture(name, icon, model,
791           width, depth, height, elevation, movable,
792           staircaseCutOutShape, color, modelRotation, backFaceShown, modelSize, creator, iconYaw, proportional);
793     }
794   }
795 
796   /**
797    * Returns the furniture category of a piece at the given <code>index</code>
798    * read from <code>preferences</code>.
799    * Caution : This method can be called from constructor so overriding implementations
800    * shouldn't be based on the state of their fields.
801    * @param preferences        the preferences from which piece of furniture data can be read
802    * @param index              the index of the read piece
803    */
readModifiableFurnitureCategory(Preferences preferences, int index)804   protected FurnitureCategory readModifiableFurnitureCategory(Preferences preferences, int index) {
805     String category = preferences.get(FURNITURE_CATEGORY + index, "");
806     return new FurnitureCategory(category);
807   }
808 
809   /**
810    * Returns model rotation parsed from key value.
811    */
getModelRotation(Preferences preferences, String key)812   private float [][] getModelRotation(Preferences preferences, String key) {
813     String modelRotationString = preferences.get(key, null);
814     if (modelRotationString == null) {
815       return new float [][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
816     } else {
817       String [] values = modelRotationString.split(" ", 9);
818       if (values.length != 9) {
819         return new float [][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
820       } else {
821         try {
822           return new float [][] {{Float.parseFloat(values [0]),
823                                   Float.parseFloat(values [1]),
824                                   Float.parseFloat(values [2])},
825                                  {Float.parseFloat(values [3]),
826                                   Float.parseFloat(values [4]),
827                                   Float.parseFloat(values [5])},
828                                  {Float.parseFloat(values [6]),
829                                   Float.parseFloat(values [7]),
830                                   Float.parseFloat(values [8])}};
831         } catch (NumberFormatException ex) {
832           return new float [][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
833         }
834       }
835     }
836   }
837 
838   /**
839    * Returns a content instance from the resource file value of key.
840    */
getContent(Preferences preferences, String key, File preferencesFolder)841   private PreferencesURLContent getContent(Preferences preferences, String key,
842                                            File preferencesFolder) {
843     String content = preferences.get(key, null);
844     if (content != null) {
845       try {
846         String preferencesFolderUrl = preferencesFolder.toURI().toURL().toString();
847         URL url;
848         if (content.startsWith(preferencesFolderUrl)
849             || content.startsWith("jar:" + preferencesFolderUrl)) {
850           url = new URL(content);
851         } else {
852           url = new URL(content.replace("file:", preferencesFolderUrl));
853         }
854         PreferencesURLContent urlContent = new PreferencesURLContent(url);
855         // Check if a local file exists
856         if (urlContent.isJAREntry()) {
857           URL jarEntryURL = urlContent.getJAREntryURL();
858           if ("file".equals(jarEntryURL.getProtocol())
859               && !new File(jarEntryURL.toURI()).exists()) {
860             return MISSING_CONTENT;
861           }
862         } else if ("file".equals(url.getProtocol())
863                    && !new File(url.toURI()).exists()) {
864           return MISSING_CONTENT;
865         }
866         return urlContent;
867       } catch (IOException ex) {
868         // Return MISSING_CONTENT for incorrect URL and content
869       } catch (URISyntaxException ex) {
870         // Return MISSING_CONTENT for incorrect content
871       }
872     }
873     return MISSING_CONTENT;
874   }
875 
876   /**
877    * Reads modifiable textures catalog from preferences.
878    */
readModifiableTexturesCatalog(Preferences preferences)879   private void readModifiableTexturesCatalog(Preferences preferences) {
880     File preferencesFolder;
881     try {
882       preferencesFolder = getPreferencesFolder();
883     } catch (IOException ex) {
884       ex.printStackTrace();
885       return;
886     }
887     CatalogTexture texture;
888     for (int i = 1; (texture = readModifiableTexture(preferences, i, preferencesFolder)) != null; i++) {
889       if (texture.getImage() != MISSING_CONTENT) {
890         TexturesCategory textureCategory = readModifiableTextureCategory(preferences, i);
891         getTexturesCatalog().add(textureCategory, texture);
892       }
893     }
894   }
895 
896   /**
897    * Returns the modifiable texture read from <code>preferences</code> at the given <code>index</code>.
898    * Caution : This method can be called from constructor so overriding implementations
899    * shouldn't be based on the state of their fields.
900    * @param preferences        the preferences from which texture data can be read
901    * @param index              the index of the read texture
902    * @param preferencesFolder  the folder where textures resources can be stored
903    * @return the read texture or <code>null</code> if the texture at the given index doesn't exist.
904    */
readModifiableTexture(Preferences preferences, int index, File preferencesFolder)905   protected CatalogTexture readModifiableTexture(Preferences preferences,
906                                                  int index, File preferencesFolder) {
907     String name = preferences.get(TEXTURE_NAME + index, null);
908     if (name == null) {
909       // Return null if key textureName# doesn't exist
910       return null;
911     }
912     Content image = getContent(preferences, TEXTURE_IMAGE + index, preferencesFolder);
913     float width = preferences.getFloat(TEXTURE_WIDTH + index, 0.1f);
914     float height = preferences.getFloat(TEXTURE_HEIGHT + index, 0.1f);
915     String creator = preferences.get(TEXTURE_CREATOR + index, null);
916     return new CatalogTexture(null, name, image, width, height, creator, true);
917   }
918 
919   /**
920    * Returns the category of a texture at the given <code>index</code>
921    * read from <code>preferences</code>.
922    * Caution : This method can be called from constructor so overriding implementations
923    * shouldn't be based on the state of their fields.
924    * @param preferences        the preferences from which texture data can be read
925    * @param index              the index of the read piece
926    */
readModifiableTextureCategory(Preferences preferences, int index)927   protected TexturesCategory readModifiableTextureCategory(Preferences preferences, int index) {
928     String category = preferences.get(TEXTURE_CATEGORY + index, "");
929     return new TexturesCategory(category);
930   }
931 
932   /**
933    * Writes user preferences in current user preferences in system.
934    */
935   @Override
write()936   public void write() throws RecorderException {
937     Preferences preferences = getPreferences();
938     writeModifiableFurnitureCatalog(preferences);
939     writeRecentAndModifiableTexturesCatalog(preferences);
940 
941     // Write other preferences
942     preferences.put(LANGUAGE, getLanguage());
943     preferences.put(EXTENSIBLE_UNIT, getLengthUnit().name());
944     String currency = getCurrency();
945     if (currency == null) {
946       preferences.remove(CURRENCY);
947     } else {
948       preferences.put(CURRENCY, currency);
949     }
950     preferences.putBoolean(VALUE_ADDED_TAX_ENABLED, isValueAddedTaxEnabled());
951     BigDecimal valueAddedTaxPercentage = getDefaultValueAddedTaxPercentage();
952     if (valueAddedTaxPercentage == null) {
953       preferences.remove(DEFAULT_VALUE_ADDED_TAX_PERCENTAGE);
954     } else {
955       preferences.put(DEFAULT_VALUE_ADDED_TAX_PERCENTAGE, valueAddedTaxPercentage.toPlainString());
956     }
957     preferences.putBoolean(FURNITURE_CATALOG_VIEWED_IN_TREE, isFurnitureCatalogViewedInTree());
958     preferences.putBoolean(NAVIGATION_PANEL_VISIBLE, isNavigationPanelVisible());
959     preferences.putBoolean(AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, isAerialViewCenteredOnSelectionEnabled());
960     preferences.putBoolean(OBSERVER_CAMERA_SELECTED_AT_CHANGE, isObserverCameraSelectedAtChange());
961     preferences.putBoolean(MAGNETISM_ENABLED, isMagnetismEnabled());
962     preferences.putBoolean(RULERS_VISIBLE, isRulersVisible());
963     preferences.putBoolean(GRID_VISIBLE, isGridVisible());
964     String defaultFontName = getDefaultFontName();
965     if (defaultFontName == null) {
966       preferences.remove(DEFAULT_FONT_NAME);
967     } else {
968       preferences.put(DEFAULT_FONT_NAME, defaultFontName);
969     }
970     preferences.putBoolean(FURNITURE_VIEWED_FROM_TOP, isFurnitureViewedFromTop());
971     preferences.putInt(FURNITURE_MODEL_ICON_SIZE, getFurnitureModelIconSize());
972     preferences.putBoolean(ROOM_FLOOR_COLORED_OR_TEXTURED, isRoomFloorColoredOrTextured());
973     preferences.put(WALL_PATTERN, getWallPattern().getName());
974     TextureImage newWallPattern = getNewWallPattern();
975     if (newWallPattern != null) {
976       preferences.put(NEW_WALL_PATTERN, newWallPattern.getName());
977     }
978     preferences.putFloat(NEW_WALL_THICKNESS, getNewWallThickness());
979     preferences.putFloat(NEW_WALL_HEIGHT, getNewWallHeight());
980     preferences.putFloat(NEW_WALL_BASEBOARD_THICKNESS, getNewWallBaseboardThickness());
981     preferences.putFloat(NEW_WALL_BASEBOARD_HEIGHT, getNewWallBaseboardHeight());
982     Integer newRoomFloorColor = getNewRoomFloorColor();
983     if (newRoomFloorColor != null) {
984       preferences.put(NEW_ROOM_FLOOR_COLOR, String.format("#%6X", newRoomFloorColor & 0xFFFFFF).replace(' ', '0'));
985     } else {
986       preferences.remove(NEW_ROOM_FLOOR_COLOR);
987     }
988     preferences.putFloat(NEW_FLOOR_THICKNESS, getNewFloorThickness());
989     preferences.putBoolean(CHECK_UPDATES_ENABLED, isCheckUpdatesEnabled());
990     Long updatesMinimumDate = getUpdatesMinimumDate();
991     if (updatesMinimumDate != null) {
992       preferences.putLong(UPDATES_MINIMUM_DATE, updatesMinimumDate);
993     }
994     preferences.putInt(AUTO_SAVE_DELAY_FOR_RECOVERY, getAutoSaveDelayForRecovery());
995     // Write recent homes list
996     int i = 1;
997     for (Iterator<String> it = getRecentHomes().iterator(); it.hasNext() && i <= getRecentHomesMaxCount(); i ++) {
998       preferences.put(RECENT_HOMES + i, it.next());
999     }
1000     // Remove obsolete keys
1001     for ( ; i <= getRecentHomesMaxCount(); i++) {
1002       preferences.remove(RECENT_HOMES + i);
1003     }
1004     // Write recent colors
1005     StringBuilder recentColors = new StringBuilder();
1006     Iterator<Integer> itColor = getRecentColors().iterator();
1007     for (int j = 0; j < 100 && itColor.hasNext(); j++) {
1008       if (j > 0) {
1009         recentColors.append(",");
1010       }
1011       recentColors.append(String.format("#%6X", itColor.next() & 0xFFFFFF).replace(' ', '0'));
1012     }
1013     preferences.put(RECENT_COLORS, recentColors.toString());
1014     // Write ignored action tips
1015     i = 1;
1016     for (Iterator<Map.Entry<String, Boolean>> it = this.ignoredActionTips.entrySet().iterator();
1017          it.hasNext(); ) {
1018       Entry<String, Boolean> ignoredActionTipEntry = it.next();
1019       if (ignoredActionTipEntry.getValue()) {
1020         preferences.put(IGNORED_ACTION_TIP + i++, ignoredActionTipEntry.getKey());
1021       }
1022     }
1023     // Remove obsolete keys
1024     for ( ; i <= this.ignoredActionTips.size(); i++) {
1025       preferences.remove(IGNORED_ACTION_TIP + i);
1026     }
1027     // Write auto completion strings lists
1028     i = 1;
1029     for (String property : getAutoCompletedProperties()) {
1030       StringBuilder autoCompletionStrings = new StringBuilder();
1031       Iterator<String> it = getAutoCompletionStrings(property).iterator();
1032       for (int j = 0; j < 1000 && it.hasNext(); j++) {
1033         String autoCompletionString = it.next();
1034         // As strings are comma separated, accept only the ones without a comma
1035         if (autoCompletionString.indexOf(',') < 0
1036             && autoCompletionStrings.length() + autoCompletionString.length() + 1 <= Preferences.MAX_VALUE_LENGTH) {
1037           if (autoCompletionStrings.length() > 0) {
1038             autoCompletionStrings.append(",");
1039           }
1040           autoCompletionStrings.append(autoCompletionString);
1041         }
1042       }
1043       preferences.put(AUTO_COMPLETION_PROPERTY + i, property);
1044       preferences.put(AUTO_COMPLETION_STRINGS + i++, autoCompletionStrings.toString());
1045     }
1046     for ( ; preferences.get(AUTO_COMPLETION_PROPERTY + i, null) != null; i++) {
1047       preferences.remove(AUTO_COMPLETION_PROPERTY + i);
1048       preferences.remove(AUTO_COMPLETION_STRINGS + i);
1049     }
1050 
1051     try {
1052       // Write preferences
1053       preferences.flush();
1054     } catch (BackingStoreException ex) {
1055       throw new RecorderException("Couldn't write preferences", ex);
1056     }
1057   }
1058 
1059   /**
1060    * Writes modifiable furniture in <code>preferences</code>.
1061    */
writeModifiableFurnitureCatalog(Preferences preferences)1062   private void writeModifiableFurnitureCatalog(Preferences preferences) throws RecorderException {
1063     final Set<URL> furnitureContentURLs = new HashSet<URL>();
1064     int i = 1;
1065     for (FurnitureCategory category : getFurnitureCatalog().getCategories()) {
1066       for (CatalogPieceOfFurniture piece : category.getFurniture()) {
1067         if (piece.isModifiable()) {
1068           preferences.put(FURNITURE_NAME + i, piece.getName());
1069           preferences.put(FURNITURE_CATEGORY + i, category.getName());
1070           putContent(preferences, FURNITURE_ICON + i, piece.getIcon(),
1071               FURNITURE_CONTENT_PREFIX, furnitureContentURLs);
1072           putContent(preferences, FURNITURE_MODEL + i, piece.getModel(),
1073               FURNITURE_CONTENT_PREFIX, furnitureContentURLs);
1074           preferences.putFloat(FURNITURE_WIDTH + i, piece.getWidth());
1075           preferences.putFloat(FURNITURE_DEPTH + i, piece.getDepth());
1076           preferences.putFloat(FURNITURE_HEIGHT + i, piece.getHeight());
1077           preferences.putFloat(FURNITURE_ELEVATION + i, piece.getElevation());
1078           preferences.putBoolean(FURNITURE_MOVABLE + i, piece.isMovable());
1079           preferences.putBoolean(FURNITURE_DOOR_OR_WINDOW + i, piece.isDoorOrWindow());
1080           if (piece.getStaircaseCutOutShape() != null) {
1081             preferences.put(FURNITURE_STAIRCASE_CUT_OUT_SHAPE + i, piece.getStaircaseCutOutShape());
1082           } else {
1083             preferences.remove(FURNITURE_STAIRCASE_CUT_OUT_SHAPE + i);
1084           }
1085           if (piece.getColor() != null) {
1086             preferences.put(FURNITURE_COLOR + i, String.valueOf(piece.getColor()));
1087           } else {
1088             preferences.remove(FURNITURE_COLOR + i);
1089           }
1090           float [][] modelRotation = piece.getModelRotation();
1091           preferences.put(FURNITURE_MODEL_ROTATION + i,
1092               floatToString(modelRotation[0][0]) + " " + floatToString(modelRotation[0][1]) + " " + floatToString(modelRotation[0][2]) + " "
1093               + floatToString(modelRotation[1][0]) + " " + floatToString(modelRotation[1][1]) + " " + floatToString(modelRotation[1][2]) + " "
1094               + floatToString(modelRotation[2][0]) + " " + floatToString(modelRotation[2][1]) + " " + floatToString(modelRotation[2][2]));
1095           preferences.putBoolean(FURNITURE_BACK_FACE_SHOWN + i, piece.isBackFaceShown());
1096           if (piece.getModelSize() != null) {
1097             preferences.putLong(FURNITURE_MODEL_SIZE + i, piece.getModelSize());
1098           } else {
1099             preferences.remove(FURNITURE_MODEL_SIZE + i);
1100           }
1101           if (piece.getCreator() != null) {
1102             preferences.put(FURNITURE_CREATOR + i, piece.getCreator());
1103           } else {
1104             preferences.remove(FURNITURE_CREATOR + i);
1105           }
1106           preferences.putFloat(FURNITURE_ICON_YAW + i, piece.getIconYaw());
1107           preferences.putBoolean(FURNITURE_PROPORTIONAL + i, piece.isProportional());
1108           i++;
1109         }
1110       }
1111     }
1112     // Remove obsolete keys
1113     for ( ; preferences.get(FURNITURE_NAME + i, null) != null; i++) {
1114       preferences.remove(FURNITURE_NAME + i);
1115       preferences.remove(FURNITURE_CATEGORY + i);
1116       preferences.remove(FURNITURE_ICON + i);
1117       preferences.remove(FURNITURE_MODEL + i);
1118       preferences.remove(FURNITURE_WIDTH + i);
1119       preferences.remove(FURNITURE_DEPTH + i);
1120       preferences.remove(FURNITURE_HEIGHT + i);
1121       preferences.remove(FURNITURE_ELEVATION + i);
1122       preferences.remove(FURNITURE_MOVABLE + i);
1123       preferences.remove(FURNITURE_DOOR_OR_WINDOW + i);
1124       preferences.remove(FURNITURE_STAIRCASE_CUT_OUT_SHAPE + i);
1125       preferences.remove(FURNITURE_COLOR + i);
1126       preferences.remove(FURNITURE_MODEL_ROTATION + i);
1127       preferences.remove(FURNITURE_BACK_FACE_SHOWN + i);
1128       preferences.remove(FURNITURE_MODEL_SIZE + i);
1129       preferences.remove(FURNITURE_CREATOR + i);
1130       preferences.remove(FURNITURE_ICON_YAW + i);
1131       preferences.remove(FURNITURE_PROPORTIONAL + i);
1132     }
1133     deleteObsoleteContent(furnitureContentURLs, FURNITURE_CONTENT_PREFIX);
1134   }
1135 
1136   /**
1137    * Returns the string value of the given float, except for -1.0, 1.0 or 0.0 where -1, 1 and 0 is returned.
1138    */
floatToString(float f)1139   private String floatToString(float f) {
1140     if (Math.abs(f) < 1E-6) {
1141       return "0";
1142     } else if (Math.abs(f - 1f) < 1E-6) {
1143       return "1";
1144     } else if (Math.abs(f + 1f) < 1E-6) {
1145       return "-1";
1146     } else {
1147       return String.valueOf(f);
1148     }
1149   }
1150 
1151   /**
1152    * Writes recent textures and modifiable textures catalog in <code>preferences</code>.
1153    */
writeRecentAndModifiableTexturesCatalog(Preferences preferences)1154   private void writeRecentAndModifiableTexturesCatalog(Preferences preferences) throws RecorderException {
1155     final Set<URL> texturesContentURLs = new HashSet<URL>();
1156     // Save recent textures
1157     int i = 1;
1158     for (TextureImage texture : getRecentTextures()) {
1159       preferences.put(RECENT_TEXTURE_NAME + i, texture.getName());
1160       putContent(preferences, RECENT_TEXTURE_IMAGE + i, texture.getImage(),
1161           TEXTURE_CONTENT_PREFIX, texturesContentURLs);
1162       if (texture.getWidth() != -1) {
1163         preferences.putFloat(RECENT_TEXTURE_WIDTH + i, texture.getWidth());
1164       } else {
1165         preferences.remove(RECENT_TEXTURE_WIDTH + i);
1166       }
1167       if (texture.getHeight() != -1) {
1168         preferences.putFloat(RECENT_TEXTURE_HEIGHT + i, texture.getHeight());
1169       } else {
1170         preferences.remove(RECENT_TEXTURE_HEIGHT + i);
1171       }
1172       if (texture.getCreator() != null) {
1173         preferences.put(RECENT_TEXTURE_CREATOR + i, texture.getCreator());
1174       } else {
1175         preferences.remove(RECENT_TEXTURE_CREATOR + i);
1176       }
1177       i++;
1178     }
1179     // Remove obsolete keys
1180     for ( ; preferences.get(RECENT_TEXTURE_NAME + i, null) != null; i++) {
1181       preferences.remove(RECENT_TEXTURE_NAME + i);
1182       preferences.remove(RECENT_TEXTURE_IMAGE + i);
1183       preferences.remove(RECENT_TEXTURE_WIDTH + i);
1184       preferences.remove(RECENT_TEXTURE_HEIGHT + i);
1185       preferences.remove(RECENT_TEXTURE_CREATOR + i);
1186     }
1187 
1188     // Save modifiable textures
1189     i = 1;
1190     for (TexturesCategory category : getTexturesCatalog().getCategories()) {
1191       for (CatalogTexture texture : category.getTextures()) {
1192         if (texture.isModifiable()) {
1193           preferences.put(TEXTURE_NAME + i, texture.getName());
1194           preferences.put(TEXTURE_CATEGORY + i, category.getName());
1195           putContent(preferences, TEXTURE_IMAGE + i, texture.getImage(),
1196               TEXTURE_CONTENT_PREFIX, texturesContentURLs);
1197           preferences.putFloat(TEXTURE_WIDTH + i, texture.getWidth());
1198           preferences.putFloat(TEXTURE_HEIGHT + i, texture.getHeight());
1199           if (texture.getCreator() != null) {
1200             preferences.put(TEXTURE_CREATOR + i, texture.getCreator());
1201           } else {
1202             preferences.remove(TEXTURE_CREATOR + i);
1203           }
1204           i++;
1205         }
1206       }
1207     }
1208     // Remove obsolete keys
1209     for ( ; preferences.get(TEXTURE_NAME + i, null) != null; i++) {
1210       preferences.remove(TEXTURE_NAME + i);
1211       preferences.remove(TEXTURE_CATEGORY + i);
1212       preferences.remove(TEXTURE_IMAGE + i);
1213       preferences.remove(TEXTURE_WIDTH + i);
1214       preferences.remove(TEXTURE_HEIGHT + i);
1215       preferences.remove(TEXTURE_CREATOR + i);
1216     }
1217 
1218     deleteObsoleteContent(texturesContentURLs, TEXTURE_CONTENT_PREFIX);
1219   }
1220 
1221   /**
1222    * Writes <code>key</code> <code>content</code> in <code>preferences</code>.
1223    */
putContent(Preferences preferences, String key, Content content, String contentPrefix, Set<URL> savedContentURLs)1224   private void putContent(Preferences preferences, String key,
1225                           Content content, String contentPrefix,
1226                           Set<URL> savedContentURLs) throws RecorderException {
1227     if (content instanceof PreferencesURLContent) {
1228       PreferencesURLContent preferencesContent = (PreferencesURLContent)content;
1229       try {
1230         preferences.put(key, preferencesContent.getURL().toString()
1231             .replace(getPreferencesFolder().toURI().toURL().toString(), "file:"));
1232       } catch (IOException ex) {
1233         throw new RecorderException("Can't save content", ex);
1234       }
1235       // Add to furnitureContentURLs the URL to the application file
1236       if (preferencesContent.isJAREntry()) {
1237         savedContentURLs.add(preferencesContent.getJAREntryURL());
1238       } else {
1239         savedContentURLs.add(preferencesContent.getURL());
1240       }
1241     } else {
1242       PreferencesURLContent preferencesContent = this.copiedContentsCache.get(content);
1243       if (preferencesContent == null) {
1244         if (content instanceof TemporaryURLContent
1245             && ((TemporaryURLContent)content).isJAREntry()) {
1246           URLContent urlContent = (URLContent)content;
1247           try {
1248             // If content is a JAR entry copy the content of its URL and rebuild a new URL content from
1249             // this copy and the entry name
1250             PreferencesURLContent copiedContent = copyToPreferencesURLContent(new URLContent(urlContent.getJAREntryURL()), contentPrefix);
1251             preferencesContent = new PreferencesURLContent(new URL("jar:" + copiedContent.getURL() + "!/" + urlContent.getJAREntryName()));
1252           } catch (MalformedURLException ex) {
1253             // Shouldn't happen
1254             throw new RecorderException("Can't build URL", ex);
1255           }
1256         } else {
1257           preferencesContent = copyToPreferencesURLContent(content, contentPrefix);
1258         }
1259         // Store the copied content in cache to avoid copying it again the next time preferences are written
1260         this.copiedContentsCache.put(content, preferencesContent);
1261       }
1262 
1263       putContent(preferences, key, preferencesContent, contentPrefix, savedContentURLs);
1264     }
1265   }
1266 
1267   /**
1268    * Returns a content object that references a copy of <code>content</code> in
1269    * user preferences folder.
1270    */
copyToPreferencesURLContent(Content content, String contentPrefix)1271   private PreferencesURLContent copyToPreferencesURLContent(Content content,
1272                                                             String contentPrefix) throws RecorderException {
1273     InputStream tempIn = null;
1274     OutputStream tempOut = null;
1275     try {
1276       File preferencesFile = createPreferencesFile(contentPrefix);
1277       tempIn = content.openStream();
1278       tempOut = new FileOutputStream(preferencesFile);
1279       byte [] buffer = new byte [8192];
1280       int size;
1281       while ((size = tempIn.read(buffer)) != -1) {
1282         tempOut.write(buffer, 0, size);
1283       }
1284       return new PreferencesURLContent(preferencesFile.toURI().toURL());
1285     } catch (IOException ex) {
1286       throw new RecorderException("Can't save content", ex);
1287     } finally {
1288       try {
1289         if (tempIn != null) {
1290           tempIn.close();
1291         }
1292         if (tempOut != null) {
1293           tempOut.close();
1294         }
1295       } catch (IOException ex) {
1296         throw new RecorderException("Can't close files", ex);
1297       }
1298     }
1299   }
1300 
1301   /**
1302    * Returns the folders where language libraries files are placed
1303    * or <code>null</code> if that folder can't be retrieved.
1304    * Caution : This method can be called from constructor so overriding implementations
1305    * shouldn't be based on the state of their fields.
1306    */
getLanguageLibrariesPluginFolders()1307   protected File [] getLanguageLibrariesPluginFolders() {
1308     try {
1309       return getApplicationSubfolders(LANGUAGE_LIBRARIES_PLUGIN_SUB_FOLDER);
1310     } catch (IOException ex) {
1311       return null;
1312     }
1313   }
1314 
1315   /**
1316    * Returns the folders where furniture catalog files are placed
1317    * or <code>null</code> if that folder can't be retrieved.
1318    * Caution : This method can be called from constructor so overriding implementations
1319    * shouldn't be based on the state of their fields.
1320    */
getFurnitureLibrariesPluginFolders()1321   protected File [] getFurnitureLibrariesPluginFolders() {
1322     try {
1323       return getApplicationSubfolders(FURNITURE_LIBRARIES_PLUGIN_SUB_FOLDER);
1324     } catch (IOException ex) {
1325       return null;
1326     }
1327   }
1328 
1329   /**
1330    * Returns the folders where texture catalog files are placed
1331    * or <code>null</code> if that folder can't be retrieved.
1332    * Caution : This method can be called from constructor so overriding implementations
1333    * shouldn't be based on the state of their fields.
1334    */
getTexturesLibrariesPluginFolders()1335   protected File [] getTexturesLibrariesPluginFolders() {
1336     try {
1337       return getApplicationSubfolders(TEXTURES_LIBRARIES_PLUGIN_SUB_FOLDER);
1338     } catch (IOException ex) {
1339       return null;
1340     }
1341   }
1342 
1343   /**
1344    * Returns the first Sweet Home 3D application folder.
1345    */
getApplicationFolder()1346   public File getApplicationFolder() throws IOException {
1347     File [] applicationFolders = getApplicationFolders();
1348     if (applicationFolders.length == 0) {
1349       throw new IOException("No application folder defined");
1350     } else {
1351       return applicationFolders [0];
1352     }
1353   }
1354 
1355   /**
1356    * Returns Sweet Home 3D application folders.
1357    * Caution : This method can be called from constructor so overriding implementations
1358    * shouldn't be based on the state of their fields.
1359    */
getApplicationFolders()1360   public File [] getApplicationFolders() throws IOException {
1361     if (this.applicationFolders != null) {
1362       return this.applicationFolders;
1363     } else {
1364       return new File [] {OperatingSystem.getDefaultApplicationFolder()};
1365     }
1366   }
1367 
1368   /**
1369    * Returns subfolders of Sweet Home 3D application folders of a given name.
1370    * Caution : This method can be called from constructor so overriding implementations
1371    * shouldn't be based on the state of their fields.
1372    */
getApplicationSubfolders(String subfolder)1373   public File [] getApplicationSubfolders(String subfolder) throws IOException {
1374     File [] applicationFolders = getApplicationFolders();
1375     File [] applicationSubfolders = new File [applicationFolders.length];
1376     for (int i = 0; i < applicationFolders.length; i++) {
1377       applicationSubfolders [i] = new File(applicationFolders [i], subfolder);
1378     }
1379     return applicationSubfolders;
1380   }
1381 
1382   /**
1383    * Returns a new file in user preferences folder.
1384    */
createPreferencesFile(String filePrefix)1385   private File createPreferencesFile(String filePrefix) throws IOException {
1386     checkPreferencesFolder();
1387     // Return a new file in preferences folder
1388     return File.createTempFile(filePrefix, ".pref", getPreferencesFolder());
1389   }
1390 
1391   /**
1392    * Creates preferences folder and its sub folders if it doesn't exist.
1393    */
checkPreferencesFolder()1394   private void checkPreferencesFolder() throws IOException {
1395     File preferencesFolder = getPreferencesFolder();
1396     // Create preferences folder if it doesn't exist
1397     if (!preferencesFolder.exists()
1398         && !preferencesFolder.mkdirs()) {
1399       throw new IOException("Couldn't create " + preferencesFolder);
1400     }
1401     checkPreferencesSubFolder(getLanguageLibrariesPluginFolders());
1402     checkPreferencesSubFolder(getFurnitureLibrariesPluginFolders());
1403     checkPreferencesSubFolder(getTexturesLibrariesPluginFolders());
1404   }
1405 
1406   /**
1407    * Creates the first folder in the given folders.
1408    */
checkPreferencesSubFolder(File [] librariesPluginFolders)1409   private void checkPreferencesSubFolder(File [] librariesPluginFolders) {
1410     if (librariesPluginFolders != null
1411         && librariesPluginFolders.length > 0
1412         && !librariesPluginFolders [0].exists()) {
1413       librariesPluginFolders [0].mkdirs();
1414     }
1415   }
1416 
1417   /**
1418    * Deletes from application folder the content files starting by <code>contentPrefix</code>
1419    * that don't belong to <code>contentURLs</code>.
1420    */
deleteObsoleteContent(final Set<URL> contentURLs, final String contentPrefix)1421   private void deleteObsoleteContent(final Set<URL> contentURLs,
1422                                      final String contentPrefix) throws RecorderException {
1423     // Search obsolete contents
1424     File applicationFolder;
1425     try {
1426       applicationFolder = getPreferencesFolder();
1427     } catch (IOException ex) {
1428       throw new RecorderException("Can't access to application folder");
1429     }
1430     File [] obsoleteContentFiles = applicationFolder.listFiles(
1431         new FileFilter() {
1432           public boolean accept(File applicationFile) {
1433             try {
1434               URL toURL = applicationFile.toURI().toURL();
1435               return applicationFile.getName().startsWith(contentPrefix)
1436                  && !contentURLs.contains(toURL);
1437             } catch (MalformedURLException ex) {
1438               return false;
1439             }
1440           }
1441         });
1442     if (obsoleteContentFiles != null) {
1443       // Delete obsolete contents at program exit to ensure removed contents
1444       // can still be saved in homes that reference them
1445       for (File file : obsoleteContentFiles) {
1446         file.deleteOnExit();
1447       }
1448     }
1449   }
1450 
1451   /**
1452    * Returns the folder where files depending on preferences are stored.
1453    */
getPreferencesFolder()1454   private File getPreferencesFolder() throws IOException {
1455     if (this.preferencesFolder != null) {
1456       return this.preferencesFolder;
1457     } else {
1458       return OperatingSystem.getDefaultApplicationFolder();
1459     }
1460   }
1461 
1462   /**
1463    * Returns default Java preferences for current system user.
1464    * Caution : This method is called once in constructor so overriding implementations
1465    * shouldn't be based on the state of their fields.
1466    */
getPreferences()1467   protected Preferences getPreferences() {
1468     if (this.preferences != null) {
1469       return this.preferences;
1470     } else {
1471       return Preferences.userNodeForPackage(FileUserPreferences.class);
1472     }
1473   }
1474 
1475   /**
1476    * Sets which action tip should be ignored.
1477    */
1478   @Override
setActionTipIgnored(String actionKey)1479   public void setActionTipIgnored(String actionKey) {
1480     this.ignoredActionTips.put(actionKey, true);
1481     super.setActionTipIgnored(actionKey);
1482   }
1483 
1484   /**
1485    * Returns whether an action tip should be ignored or not.
1486    */
1487   @Override
isActionTipIgnored(String actionKey)1488   public boolean isActionTipIgnored(String actionKey) {
1489     Boolean ignoredActionTip = this.ignoredActionTips.get(actionKey);
1490     return ignoredActionTip != null && ignoredActionTip.booleanValue();
1491   }
1492 
1493   /**
1494    * Resets the display flag of action tips.
1495    */
1496   @Override
resetIgnoredActionTips()1497   public void resetIgnoredActionTips() {
1498     for (Iterator<Map.Entry<String, Boolean>> it = this.ignoredActionTips.entrySet().iterator();
1499          it.hasNext(); ) {
1500       Entry<String, Boolean> ignoredActionTipEntry = it.next();
1501       ignoredActionTipEntry.setValue(false);
1502     }
1503     super.resetIgnoredActionTips();
1504   }
1505 
1506   /**
1507    * Returns <code>true</code> if the language library at the given location exists
1508    * in the first language libraries folder.
1509    * @param location the file path of the resource to check
1510    */
languageLibraryExists(String location)1511   public boolean languageLibraryExists(String location) throws RecorderException {
1512     File [] languageLibrariesPluginFolders = getLanguageLibrariesPluginFolders();
1513     if (languageLibrariesPluginFolders == null
1514         || languageLibrariesPluginFolders.length == 0) {
1515       throw new RecorderException("Can't access to language libraries plugin folder");
1516     } else {
1517       String libraryFileName = new File(location).getName();
1518       return new File(languageLibrariesPluginFolders [0], libraryFileName).exists();
1519     }
1520   }
1521 
1522   /**
1523    * Adds <code>languageLibraryPath</code> to the first language libraries folder
1524    * to make the language library it contains available to supported languages.
1525    */
addLanguageLibrary(String languageLibraryPath)1526   public void addLanguageLibrary(String languageLibraryPath) throws RecorderException {
1527     try {
1528       File [] languageLibrariesPluginFolders = getLanguageLibrariesPluginFolders();
1529       if (languageLibrariesPluginFolders == null
1530           || languageLibrariesPluginFolders.length == 0) {
1531         throw new RecorderException("Can't access to language libraries plugin folder");
1532       }
1533       copyToLibraryFolder(new File(languageLibraryPath), languageLibrariesPluginFolders [0]);
1534       updateSupportedLanguages();
1535     } catch (IOException ex) {
1536       throw new RecorderException(
1537           "Can't write " + languageLibraryPath +  " in language libraries plugin folder", ex);
1538     }
1539   }
1540 
1541   /**
1542    * Returns <code>true</code> if the furniture library at the given <code>location</code> exists
1543    * in the first furniture libraries folder.
1544    * @param location the file path of the resource to check
1545    */
1546   @Override
furnitureLibraryExists(String location)1547   public boolean furnitureLibraryExists(String location) throws RecorderException {
1548     File [] furnitureLibrariesPluginFolders = getFurnitureLibrariesPluginFolders();
1549     if (furnitureLibrariesPluginFolders == null
1550         || furnitureLibrariesPluginFolders.length == 0) {
1551       throw new RecorderException("Can't access to furniture libraries plugin folder");
1552     } else {
1553       String libraryFileName = new File(location).getName();
1554       return new File(furnitureLibrariesPluginFolders [0], libraryFileName).exists();
1555     }
1556   }
1557 
1558   /**
1559    * Adds the file <code>furnitureLibraryPath</code> to the first furniture libraries folder
1560    * to make the furniture library available to catalog.
1561    */
1562   @Override
addFurnitureLibrary(String furnitureLibraryPath)1563   public void addFurnitureLibrary(String furnitureLibraryPath) throws RecorderException {
1564     try {
1565       File [] furnitureLibrariesPluginFolders = getFurnitureLibrariesPluginFolders();
1566       if (furnitureLibrariesPluginFolders == null
1567           || furnitureLibrariesPluginFolders.length == 0) {
1568         throw new RecorderException("Can't access to furniture libraries plugin folder");
1569       }
1570       copyToLibraryFolder(new File(furnitureLibraryPath), furnitureLibrariesPluginFolders [0]);
1571       updateFurnitureDefaultCatalog(this.catalogsLoader, this.updater);
1572     } catch (IOException ex) {
1573       throw new RecorderException(
1574           "Can't write " + furnitureLibraryPath +  " in furniture libraries plugin folder", ex);
1575     }
1576   }
1577 
1578   /**
1579    * Returns <code>true</code> if the textures library at the given <code>location</code> exists
1580    * in the first textures libraries folder.
1581    * @param location the file path of the resource to check
1582    */
1583   @Override
texturesLibraryExists(String location)1584   public boolean texturesLibraryExists(String location) throws RecorderException {
1585     File [] texturesLibrariesPluginFolders = getTexturesLibrariesPluginFolders();
1586     if (texturesLibrariesPluginFolders == null
1587         || texturesLibrariesPluginFolders.length == 0) {
1588       throw new RecorderException("Can't access to textures libraries plugin folder");
1589     } else {
1590       String libraryLocation = new File(location).getName();
1591       return new File(texturesLibrariesPluginFolders [0], libraryLocation).exists();
1592     }
1593   }
1594 
1595   /**
1596    * Adds the file <code>texturesLibraryPath</code> to the first textures libraries folder
1597    * to make the textures library available to catalog.
1598    */
1599   @Override
addTexturesLibrary(String texturesLibraryPath)1600   public void addTexturesLibrary(String texturesLibraryPath) throws RecorderException {
1601     try {
1602       File [] texturesLibrariesPluginFolders = getTexturesLibrariesPluginFolders();
1603       if (texturesLibrariesPluginFolders == null
1604           || texturesLibrariesPluginFolders.length == 0) {
1605         throw new RecorderException("Can't access to textures libraries plugin folder");
1606       }
1607       copyToLibraryFolder(new File(texturesLibraryPath), texturesLibrariesPluginFolders [0]);
1608       updateTexturesDefaultCatalog(this.catalogsLoader, this.updater);
1609     } catch (IOException ex) {
1610       throw new RecorderException(
1611           "Can't write " + texturesLibraryPath +  " in textures libraries plugin folder", ex);
1612     }
1613   }
1614 
1615   /**
1616    * Copies a library file to a folder.
1617    */
copyToLibraryFolder(File libraryFile, File folder)1618   private void copyToLibraryFolder(File libraryFile, File folder) throws IOException {
1619     String libraryFileName = libraryFile.getName();
1620     File destinationFile = new File(folder, libraryFileName);
1621     if (destinationFile.exists()) {
1622       // Delete file to reinitialize handlers
1623       destinationFile.delete();
1624     }
1625     InputStream tempIn = null;
1626     OutputStream tempOut = null;
1627     try {
1628       tempIn = new BufferedInputStream(new FileInputStream(libraryFile));
1629       // Create folder if it doesn't exist
1630       folder.mkdirs();
1631       tempOut = new FileOutputStream(destinationFile);
1632       byte [] buffer = new byte [8192];
1633       int size;
1634       while ((size = tempIn.read(buffer)) != -1) {
1635         tempOut.write(buffer, 0, size);
1636       }
1637     } finally {
1638       if (tempIn != null) {
1639         tempIn.close();
1640       }
1641       if (tempOut != null) {
1642         tempOut.close();
1643       }
1644     }
1645   }
1646 
1647   /**
1648    * Returns the libraries available in user preferences.
1649    * @since 4.0
1650    */
1651   @Override
getLibraries()1652   public List<Library> getLibraries() {
1653     return Collections.unmodifiableList(new ArrayList<Library>(this.libraries));
1654   }
1655 
1656   /**
1657    * Deletes the given <code>libraries</code> and updates user preferences.
1658    * @since 4.0
1659    */
deleteLibraries(List<Library> libraries)1660   public void deleteLibraries(List<Library> libraries) throws RecorderException {
1661     boolean updateFurnitureCatalog = false;
1662     boolean updateTexturesCatalog  = false;
1663     boolean updateSupportedLanguages = false;
1664     for (Library library : libraries) {
1665       if (!new File(library.getLocation()).delete()) {
1666         throw new RecorderException("Couldn't delete file " + library.getLocation());
1667       } else {
1668         if (FURNITURE_LIBRARY_TYPE.equals(library.getType())) {
1669           updateFurnitureCatalog = true;
1670         } else if (TEXTURES_LIBRARY_TYPE.equals(library.getType())) {
1671           updateTexturesCatalog = true;
1672         }  else if (LANGUAGE_LIBRARY_TYPE.equals(library.getType())) {
1673           updateSupportedLanguages = true;
1674         }
1675       }
1676     }
1677 
1678     if (updateFurnitureCatalog) {
1679       updateFurnitureDefaultCatalog(this.catalogsLoader, this.updater);
1680     }
1681     if (updateTexturesCatalog) {
1682       updateTexturesDefaultCatalog(this.catalogsLoader, this.updater);
1683     }
1684     if (updateSupportedLanguages) {
1685       updateSupportedLanguages();
1686     }
1687   }
1688 
1689   /**
1690    * Returns <code>true</code> if the given file <code>library</code> can be deleted.
1691    * @since 4.0
1692    */
isLibraryDeletable(Library library)1693   public boolean isLibraryDeletable(Library library) {
1694     return new File(library.getLocation()).canWrite();
1695   }
1696 
1697 
1698   /**
1699    * A content stored in preferences.
1700    */
1701   private static class PreferencesURLContent extends URLContent {
PreferencesURLContent(URL url)1702     public PreferencesURLContent(URL url) {
1703       super(url);
1704     }
1705   }
1706 
1707 
1708   /**
1709    * Preferences based on the <code>preferences.xml</code> file
1710    * stored in a preferences folder.
1711    */
1712   private class PortablePreferences extends AbstractPreferences {
1713     private static final String PREFERENCES_FILE = "preferences.xml";
1714 
1715     private Properties  preferencesProperties;
1716     private boolean     exist;
1717 
PortablePreferences()1718     private PortablePreferences() {
1719       super(null, "");
1720       this.preferencesProperties = new Properties();
1721       this.exist = readPreferences();
1722     }
1723 
exist()1724     public boolean exist() {
1725       return this.exist;
1726     }
1727 
1728     @Override
syncSpi()1729     protected void syncSpi() throws BackingStoreException {
1730       this.preferencesProperties.clear();
1731       this.exist = readPreferences();
1732     }
1733 
1734     @Override
removeSpi(String key)1735     protected void removeSpi(String key) {
1736       this.preferencesProperties.remove(key);
1737     }
1738 
1739     @Override
putSpi(String key, String value)1740     protected void putSpi(String key, String value) {
1741       this.preferencesProperties.put(key, value);
1742     }
1743 
1744     @Override
keysSpi()1745     protected String [] keysSpi() throws BackingStoreException {
1746       return this.preferencesProperties.keySet().toArray(new String [0]);
1747     }
1748 
1749     @Override
getSpi(String key)1750     protected String getSpi(String key) {
1751       return (String)this.preferencesProperties.get(key);
1752     }
1753 
1754     @Override
flushSpi()1755     protected void flushSpi() throws BackingStoreException {
1756       try {
1757         writePreferences();
1758       } catch (IOException ex) {
1759         throw new BackingStoreException(ex);
1760       }
1761     }
1762 
1763     @Override
removeNodeSpi()1764     protected void removeNodeSpi() throws BackingStoreException {
1765       throw new UnsupportedOperationException();
1766     }
1767 
1768     @Override
childrenNamesSpi()1769     protected String [] childrenNamesSpi() throws BackingStoreException {
1770       throw new UnsupportedOperationException();
1771     }
1772 
1773     @Override
childSpi(String name)1774     protected AbstractPreferences childSpi(String name) {
1775       throw new UnsupportedOperationException();
1776     }
1777 
1778     /**
1779      * Reads user preferences.
1780      */
readPreferences()1781     private boolean readPreferences() {
1782       InputStream in = null;
1783       try {
1784         in = new FileInputStream(new File(getPreferencesFolder(), PREFERENCES_FILE));
1785         this.preferencesProperties.loadFromXML(in);
1786         return true;
1787       } catch (IOException ex) {
1788         // Preferences don't exist
1789         return false;
1790       } finally {
1791         try {
1792           if (in != null) {
1793             in.close();
1794           }
1795         } catch (IOException ex) {
1796           // Let default preferences unchanged
1797         }
1798       }
1799     }
1800 
1801     /**
1802      * Writes user preferences.
1803      */
writePreferences()1804     private void writePreferences() throws IOException {
1805       OutputStream out = null;
1806       try {
1807         checkPreferencesFolder();
1808         out = new FileOutputStream(new File(getPreferencesFolder(), PREFERENCES_FILE));
1809         this.preferencesProperties.storeToXML(out, "Portable user preferences 3.0");
1810       } finally {
1811         if (out != null) {
1812           out.close();
1813           this.exist = true;
1814         }
1815       }
1816     }
1817   }
1818 }
1819