1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package com.commsen.apropos.core;
20
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.LinkedList;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Properties;
32
33 import org.apache.commons.io.FileUtils;
34 import org.apache.commons.lang.StringUtils;
35 import org.apache.commons.lang.SystemUtils;
36
37 import com.thoughtworks.xstream.XStream;
38
39 /***
40 * The class provides static synchronized methods for common operations on {@link Property} and
41 * {@link PropertyPackage} like add, delete, update, etc. It is also responsible for persisting data
42 * after any change and loading data form storage on startup.
43 *
44 * @author Milen Dyankov
45 *
46 */
47 public class PropertiesManager {
48
49 /***
50 * A folder to persists data into
51 */
52 private static final File storage = new File(SystemUtils.getUserHome(), ".apropos");
53
54 /***
55 * The name of the file containing data
56 */
57 private static final File dataFile = new File(storage, "data.xml");
58
59 /***
60 *
61 */
62 private static XStream xStream = new XStream();
63
64 /***
65 * Map of all packages. Key is package name
66 */
67 private static Map<String, PropertyPackage> allPackages = new HashMap<String, PropertyPackage>();
68
69 /***
70 * The singletone instance used to persist data into file
71 */
72 private static PropertiesManager instance = new PropertiesManager();
73
74 static {
75 xStream.alias("package", PropertyPackage.class);
76
77 try {
78 FileUtils.forceMkdir(storage);
79 } catch (IOException e) {
80 }
81 load();
82 }
83
84 /***
85 * List of all top level packages
86 */
87 private List<PropertyPackage> rootPackages = new LinkedList<PropertyPackage>();
88
89
90 /***
91 * Private constructor.
92 */
93 private PropertiesManager() {
94
95 }
96
97
98 /***
99 * Loads properties form {@value #dataFile} and fills {@link #rootPackages} and
100 * {@value #allPackages}. This method is called only once - during class initialization (from a
101 * static block)
102 */
103 private static void load() {
104 if (dataFile.exists() && dataFile.isFile()) {
105 FileInputStream dataStream = null;
106 try {
107 dataStream = new FileInputStream(dataFile);
108 instance = (PropertiesManager) xStream.fromXML(dataStream);
109 } catch (FileNotFoundException e) {
110 throw new InternalError(e.getMessage());
111 } finally {
112 if (dataStream != null) try {
113 dataStream.close();
114 } catch (IOException e) {
115
116 }
117 }
118 for (PropertyPackage rootPackage : instance.rootPackages) {
119 addToAllPackages(rootPackage);
120 }
121 }
122 }
123
124
125 /***
126 * Adds a package and all of it's children to {@link #allPackages}.
127 *
128 * @param propertyPackage the package to add
129 */
130 private static void addToAllPackages(PropertyPackage propertyPackage) {
131 if (propertyPackage == null) return;
132 allPackages.put(propertyPackage.getName(), propertyPackage);
133 if (propertyPackage.getChildren() != null) {
134 for (PropertyPackage child : propertyPackage.getChildren()) {
135 addToAllPackages(child);
136 }
137 }
138 }
139
140
141 /***
142 * Deletes a package and all of it's children from {@link #allPackages}.
143 *
144 * @param propertyPackage the package to add
145 */
146 private static void deleteFromAllPackages(PropertyPackage propertyPackage) {
147 if (propertyPackage == null) return;
148 if (propertyPackage.getChildren() != null) {
149 for (PropertyPackage child : propertyPackage.getChildren()) {
150 deleteFromAllPackages(child);
151 }
152 }
153 allPackages.remove(propertyPackage.getName());
154 }
155
156
157 /***
158 * Saves data into {@link #dataFile}. This method is called by other static methods of this
159 * class after modifying any data.
160 */
161 private static void save() {
162 FileOutputStream fileStream = null;
163 try {
164 fileStream = new FileOutputStream(dataFile);
165 xStream.toXML(instance, fileStream);
166 } catch (FileNotFoundException e) {
167 throw new InternalError(e.getMessage());
168 } finally {
169 if (fileStream != null) try {
170 fileStream.close();
171 } catch (IOException e) {
172
173 }
174 }
175 }
176
177
178 /***
179 * Adds new {@link PropertyPackage}. NOTE: The object passed is cloned internally.
180 *
181 * @param propertyPackage the {@link PropertyPackage} to be added. Can not be null.
182 * @throws PropertiesException if package with that name already exists or if circular
183 * parent/child relation is detected.
184 * @throws IllegalArgumentException if <code>propertyPackage</code> is null
185 */
186 public static synchronized void addPropertyPackage(PropertyPackage propertyPackage) throws PropertiesException {
187 if (propertyPackage == null) throw new IllegalArgumentException("propertySet can not be null");
188 String key = propertyPackage.getName();
189 if (allPackages.containsKey(key)) throw new PropertiesException("Configuration called " + key + " already exists");
190
191 String parentPackageName = null;
192 if (propertyPackage.getParent() != null) {
193 parentPackageName = propertyPackage.getParent().getName();
194 propertyPackage.setParent(null);
195 }
196
197 PropertyPackage clonedPackage = null;
198 try {
199 clonedPackage = (PropertyPackage) propertyPackage.clone();
200 } catch (CloneNotSupportedException e) {
201 throw new InternalError(e.toString());
202 }
203
204 clonedPackage.setParent(allPackages.get(parentPackageName));
205 allPackages.put(key, clonedPackage);
206 if (clonedPackage.getParent() == null) {
207 instance.rootPackages.add(clonedPackage);
208 }
209
210 save();
211 }
212
213
214 /***
215 * Returns unmodifiable (read only) list of all top level packages
216 *
217 * @return unmodifiable (read only) list of all top level packages
218 */
219 public static List<PropertyPackage> getRootPropertyPackages() {
220 return Collections.unmodifiableList(instance.rootPackages);
221 }
222
223
224 /***
225 * Returns unmodifiable (read only) list of all package names
226 *
227 * @return unmodifiable (read only) list of all package names
228 */
229 public static List<String> getPropertyPackagesNames() {
230 LinkedList<String> result = new LinkedList<String>(allPackages.keySet());
231 Collections.sort(result);
232 return Collections.unmodifiableList(result);
233 }
234
235
236 /***
237 * Returns a clone of {@link PropertyPackage} called <code>name</code>. If no such
238 * {@link PropertyPackage} exists it will return <code>null</code>.
239 *
240 * @return a {@link PropertyPackage} called <code>name</code> or <code>null</code>.
241 */
242 public static PropertyPackage getPropertyPackage(String name) {
243 try {
244 PropertyPackage propertyPackage = allPackages.get(name);
245 return propertyPackage == null ? null : (PropertyPackage) propertyPackage.clone();
246 } catch (CloneNotSupportedException e) {
247 throw new InternalError(e.getMessage());
248 }
249 }
250
251
252 /***
253 * Deletes the {@link PropertyPackage} specified by <code>packageName</code> and all it's
254 * children.
255 *
256 * @param packageName
257 * @throws PropertiesException
258 */
259 public static synchronized void deletePropertyPackage(String packageName) throws PropertiesException {
260 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
261 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
262 PropertyPackage propertyPackage = allPackages.get(packageName);
263 if (propertyPackage.getParent() != null) {
264 propertyPackage.setParent(null);
265 } else {
266 instance.rootPackages.remove(propertyPackage);
267 }
268 deleteFromAllPackages(propertyPackage);
269 save();
270 }
271
272
273 /***
274 * Adds new {@link Property} in {@link PropertyPackage} called <code>packageName</code> by
275 * calling {@link PropertyPackage#addProperty(Property)}.
276 *
277 * @param packageName the name of the package. Can not be <code>null</code> or empty.
278 * @param property the {@link Property} to add. Can not be <code>null</code>
279 * @return a new clone of the {@link PropertyPackage} made after the property is added
280 * @throws PropertiesException if no package called <code>packageName</code> found or if such
281 * {@link Property} already exists in this package
282 * @throws IllegalArgumentException if any of the arguments is <code>null</code>
283 */
284 public static synchronized PropertyPackage addProperty(String packageName, Property property) throws PropertiesException {
285 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
286 if (property == null) throw new IllegalArgumentException("property can not be null");
287 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
288 PropertyPackage propertyPackage = allPackages.get(packageName);
289 try {
290 propertyPackage.addProperty((Property) property.clone());
291 save();
292 return (PropertyPackage) propertyPackage.clone();
293 } catch (CloneNotSupportedException e) {
294 throw new InternalError(e.toString());
295 }
296 }
297
298
299 /***
300 * Updates {@link Property} in {@link PropertyPackage} called <code>packageName</code>. The
301 * actual update is handled by {@link PropertyPackage#updateProperty(Property)}
302 *
303 * @param packageName the name of the package. Can not be <code>null</code> or empty.
304 * @param property the {@link Property} to be updated. Can not be <code>null</code>
305 * @return a new clone of the {@link PropertyPackage} made after the property is updated
306 * @throws PropertiesException if no package called <code>packageName</code> found or if no
307 * such {@link Property} exists in this package
308 * @throws IllegalArgumentException if any of the arguments is <code>null</code>
309 */
310 public static synchronized PropertyPackage updateProperty(String packageName, Property property) throws PropertiesException {
311 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
312 if (property == null) throw new IllegalArgumentException("property can not be null");
313 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
314 PropertyPackage propertyPackage = allPackages.get(packageName);
315 try {
316 propertyPackage.updateProperty((Property) property.clone());
317 save();
318 return (PropertyPackage) propertyPackage.clone();
319 } catch (CloneNotSupportedException e) {
320 throw new InternalError(e.toString());
321 }
322 }
323
324
325 /***
326 * Updates existing {@link Property} called <code>oldPropertyName</code> in
327 * {@link PropertyPackage} called <code>packageName</code> with values from
328 * <code>property</code>. The actual update is handled by
329 * {@link PropertyPackage#updateProperty(Property)}.
330 *
331 * @param packageName the name of the package. Can not be <code>null</code> or empty.
332 * @param oldPropertyName the name of the property to be updated
333 * @param property the {@link Property} object to get the values from. Can not be
334 * <code>null</code>
335 * @return a new clone of the {@link PropertyPackage} made after the property is updated
336 * @throws PropertiesException if no package called <code>packageName</code> found or if no
337 * such {@link Property} exists in this package
338 * @throws IllegalArgumentException if any of the arguments is <code>null</code>
339 */
340 public static synchronized PropertyPackage updateProperty(String packageName, String oldPropertyName, Property property) throws PropertiesException {
341 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
342 if (property == null) throw new IllegalArgumentException("property can not be null");
343 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
344 PropertyPackage propertyPackage = allPackages.get(packageName);
345 try {
346 propertyPackage.updateProperty(oldPropertyName, (Property) property.clone());
347 save();
348 return (PropertyPackage) propertyPackage.clone();
349 } catch (CloneNotSupportedException e) {
350 throw new InternalError(e.toString());
351 }
352 }
353
354
355 /***
356 * Imports external properties into {@link PropertyPackage} called <code>packageName</code> .
357 * The actual import is handled by {@link PropertyPackage#importProperties(Properties, boolean)}.
358 *
359 * @param packageName the name of the package. Can not be <code>null</code> or empty.
360 * @param externalProperties the properties to import. This can not be <code>null</code>
361 * @param overwrite boolean flag indicates whether to override existing properties
362 * @return a new clone of the {@link PropertyPackage} made after the properties are imported
363 * @throws PropertiesException if there is no package called <code>packageName</code>
364 * @throws IllegalArgumentException if any of the arguments is <code>null</code>
365 */
366 public static synchronized PropertyPackage importProperties(String packageName, Properties externalProperties, boolean overwrite) throws PropertiesException {
367 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
368 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
369 PropertyPackage propertyPackage = allPackages.get(packageName);
370 try {
371 propertyPackage.importProperties(externalProperties, overwrite);
372 save();
373 return (PropertyPackage) propertyPackage.clone();
374 } catch (CloneNotSupportedException e) {
375 throw new InternalError(e.toString());
376 }
377 }
378
379
380 /***
381 * Deletes {@link Property} called <code>propertyName</code> from {@link PropertyPackage}
382 * called <code>packageName</code>. The actual deletion is handled by
383 * {@link PropertyPackage#removeProperty(String)}.
384 *
385 * @param packageName the name of the package. Can not be <code>null</code> or empty.
386 * @param propertyName the name of the property to be updated
387 * @return a new clone of the {@link PropertyPackage} made after the property is deleted
388 * @throws PropertiesException if there is no package called <code>packageName</code> or no
389 * property called <code>propertyName</code>
390 * @throws IllegalArgumentException if any of the arguments is <code>null</code>
391 */
392 public static synchronized PropertyPackage deleteProperty(String packageName, String propertyName) throws PropertiesException {
393 if (StringUtils.isBlank(packageName)) throw new IllegalArgumentException("package name can not be null nor empty string");
394 if (StringUtils.isBlank(propertyName)) throw new IllegalArgumentException("property can not be null nor empty string");
395 if (!allPackages.containsKey(packageName)) throw new PropertiesException("No package called " + packageName + " found ");
396 PropertyPackage propertyPackage = allPackages.get(packageName);
397 propertyPackage.removeProperty(propertyName);
398 save();
399 try {
400 return (PropertyPackage) propertyPackage.clone();
401 } catch (CloneNotSupportedException e) {
402 throw new InternalError(e.toString());
403 }
404 }
405
406 }