View Javadoc

1   /*
2    * Copyright 2008 COMMSEN International
3    *
4    * This file is part of APropOS.
5    * 
6    * APropOS is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Lesser General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * APropOS 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 Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public License
17   * along with APropOS.  If not, see <http://www.gnu.org/licenses/>.
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  		// private constructor
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 					// oops failed to close stream
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 				// oops failed to close stream
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 		// packages.add(propertyPackage);
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); // to delete it from parent's collection
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 }