View Javadoc

1   /*
2    *   Firemox is a turn based strategy simulator
3    *   Copyright (C) 2003-2007 Fabrice Daugan
4    *
5    *   This program is free software; you can redistribute it and/or modify it 
6    * under the terms of the GNU General Public License as published by the Free 
7    * Software Foundation; either version 2 of the License, or (at your option) any
8    * later version.
9    *
10   *   This program is distributed in the hope that it will be useful, but WITHOUT 
11   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12   * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
13   * details.
14   *
15   *   You should have received a copy of the GNU General Public License along  
16   * with this program; if not, write to the Free Software Foundation, Inc., 
17   * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18   */
19  package net.sf.firemox.database;
20  
21  import java.awt.Image;
22  import java.awt.image.BufferedImage;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.PrintWriter;
27  import java.lang.reflect.AccessibleObject;
28  import java.lang.reflect.Field;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import net.sf.firemox.clickable.target.card.CardModel;
35  import net.sf.firemox.database.data.StringData;
36  import net.sf.firemox.database.propertyconfig.Cache;
37  import net.sf.firemox.database.propertyconfig.PropertyConfig;
38  import net.sf.firemox.database.propertyconfig.PropertyConfigFactory;
39  import net.sf.firemox.token.IdConst;
40  import net.sf.firemox.tools.Configuration;
41  import net.sf.firemox.tools.FileFilterPlus;
42  import net.sf.firemox.tools.Log;
43  import net.sf.firemox.tools.MToolKit;
44  import net.sf.firemox.xml.XmlParser;
45  import net.sf.firemox.xml.XmlTools;
46  import net.sf.firemox.xml.XmlParser.Node;
47  
48  import org.apache.commons.io.IOUtils;
49  import org.xml.sax.SAXException;
50  
51  import sun.awt.image.FileImageSource;
52  import sun.awt.image.URLImageSource;
53  
54  /***
55   * @author <a href="mailto:fabdouglas@users.sourceforge.net">Fabrice Daugan </a>
56   * @since 0.90
57   */
58  public final class DatabaseFactory {
59  
60  	private static final String CACHE_FILE_HEADER = "<?xml version='1.0' encoding='ISO-8859-1'?>";
61  
62  	/***
63  	 * Unique instance of a XML database.
64  	 */
65  	private static XmlParser.Node config;
66  
67  	/***
68  	 * The loaded proxy configurations. The order determine priorities.
69  	 */
70  	public static Proxy[] pictureProxies;
71  
72  	/***
73  	 * The loaded proxy configurations. The order determine priorities.
74  	 */
75  	public static Proxy[] dataProxies;
76  
77  	/***
78  	 * A blank blacked-image.
79  	 */
80  	public static Image blankImage;
81  
82  	/***
83  	 * Available properties of current TBS for databases.
84  	 */
85  	public static Map<String, PropertyConfig> propertiesCacheConfig;
86  
87  	/***
88  	 * The common back picture of the cards of the selected turn based system. Is
89  	 * <code>null</code> while this factory has not been initialized.
90  	 * 
91  	 * @see #init(InputStream)
92  	 */
93  	public static Image backImage;
94  
95  	/***
96  	 * The common scaled back picture of the cards of the selected turn based
97  	 * system. Is <code>null</code> while this factory has not been initialized.
98  	 */
99  	public static BufferedImage scaledBackImage;
100 
101 	/***
102 	 * The picture representing a damage for the selected turn based system. Is
103 	 * <code>null</code> while this factory has not been initialized.
104 	 * 
105 	 * @see #init(InputStream)
106 	 */
107 	public static Image damageImage;
108 
109 	/***
110 	 * The picture representing a damage for the selected turn based system. Is
111 	 * <code>null</code> while this factory has not been initialized.
112 	 * 
113 	 * @see #init(InputStream)
114 	 */
115 	public static Image damageScaledImage;
116 
117 	/***
118 	 * The source file field allowing to access the source file/URL of any
119 	 * picture.
120 	 */
121 	static Field sourceFile;
122 
123 	static Field sourceUrl;
124 
125 	/***
126 	 * All created instances of DatabaseCard.
127 	 */
128 	private static Map<Node, DatabaseCard> databaseCardCache = new HashMap<Node, DatabaseCard>();
129 
130 	/***
131 	 * Return the DatabaseCard object from the given card name. This object is
132 	 * build from the cache or the available proxies if cache is empty for this
133 	 * card. If there is no available proxy or if all all failed, the null value
134 	 * is returned. In case of success, the cache is updated and further proxy
135 	 * call would be done.
136 	 * 
137 	 * @param pictureName
138 	 *          the picture name associated to the database object. May be null to
139 	 *          use the picture associated to the given card instead.
140 	 * @param cardModel
141 	 *          the card name (no translated one)
142 	 * @param constraints
143 	 *          constraints set {key,value}. May be null.
144 	 * @return the DatabaseCard object from the given card name.
145 	 */
146 	public static DatabaseCard getDatabase(String pictureName,
147 			CardModel cardModel, Map<String, String> constraints) {
148 
149 		// first retrieve data from cache
150 		DatabaseCard cacheData = null;
151 		try {
152 			cacheData = getDatabaseFromCache(cardModel, constraints);
153 		} catch (Exception e1) {
154 			e1.printStackTrace();
155 		}
156 
157 		if (cacheData != null && cacheData.isConsistent()) {
158 			// ok, this card is already in the cache, return it
159 			if (pictureName != null && pictureName.length() > 0) {
160 				// The picture of this card must be redefined
161 				cacheData = cacheData.clone(pictureName);
162 			}
163 			return cacheData;
164 		}
165 
166 		// return new DatabaseCardEmpty(cardName);;
167 
168 		// iterate data proxies according preferences
169 		for (Proxy dataProxy : dataProxies) {
170 			if (dataProxy != null) {
171 				try {
172 					// get data from this proxy
173 					final DatabaseCard res = getDatabaseFromProxy(dataProxy, cardModel,
174 							constraints);
175 					if (res == null) {
176 						return cacheData;
177 					}
178 					res.setPictureProxies(pictureProxies);
179 
180 					// Complete the cache with the defined user properties
181 					if (constraints != null) {
182 						for (Map.Entry<String, String> key : constraints.entrySet()) {
183 							res.add(new StringData(new Cache(key.getKey()), key.getValue()));
184 						}
185 					}
186 					updateCache(res);
187 					return res;
188 				} catch (Exception e) {
189 					// this proxy failed, we ignore this error and try the next one
190 					e.printStackTrace();
191 					Log.info("Proxy-data failure for " + cardModel + " : "
192 							+ dataProxy.getName() + ", error=" + e.getMessage());
193 				}
194 			}
195 		}
196 		// No valid proxy, maybe no available connection
197 		return null;
198 	}
199 
200 	/***
201 	 * Retrieve data from the cache. If this card is not stored in cache, null
202 	 * value is returned.
203 	 * 
204 	 * @param cardName
205 	 *          the card name (no translated one)
206 	 * @param constraints
207 	 *          constraints set{key,value}. May be null.
208 	 * @return the DatabaseCard object from the given card name from the cache.
209 	 */
210 	private static DatabaseCard getDatabaseFromCache(CardModel cardModel,
211 			Map<String, String> constraints) throws IOException, SAXException {
212 		if (config == null) {
213 			final XmlParser parser = new XmlParser();
214 			Configuration.loadTemplateTbsFile(IdConst.FILE_DATABASE_SAVED);
215 			config = parser.parse(MToolKit.getTbsUrl(IdConst.FILE_DATABASE_SAVED)
216 					.getPath());
217 		}
218 
219 		final List<Node> cachedInstances = config.getNodes(cardModel.getKeyName());
220 		if (cachedInstances != null && !cachedInstances.isEmpty()) {
221 			final List<Node> validateCards = new ArrayList<Node>();
222 
223 			// This card is already cached once, keep only the best one
224 			int bestScore = 0;
225 			for (Node cachedCard : cachedInstances) {
226 				// Iterate over instances of this card assuming constraints.
227 				int score = 0;
228 				if (constraints != null && !constraints.isEmpty()) {
229 					for (Map.Entry<String, String> constraintKey : constraints.entrySet()) {
230 						final String constraintValue = constraintKey.getValue();
231 						if (constraintValue == null || constraintValue.length() == 0) {
232 							// Invalid constraint ?!
233 							throw new InternalError("No value associated to property '"
234 									+ constraintKey.getKey() + "'");
235 						}
236 						// Check the constraint against cached data of this card
237 						final List<Node> properties = cachedCard.getNodes("property");
238 						if (properties != null && !properties.isEmpty()) {
239 							for (Node property : properties) {
240 								if (property.getAttribute("name").equalsIgnoreCase(
241 										constraintKey.getKey())
242 										&& property.getAttribute("value").equalsIgnoreCase(
243 												constraintValue)) {
244 									// the requested property has been found and does not match
245 									score++;
246 									break;
247 								}
248 							}
249 						}
250 					}
251 				} else {
252 					validateCards.add(cachedCard);
253 					break;
254 				}
255 				if (score > bestScore) {
256 					bestScore = score;
257 					validateCards.clear();
258 					validateCards.add(cachedCard);
259 				} else if (score == bestScore) {
260 					validateCards.add(cachedCard);
261 				}
262 			}
263 			if (!validateCards.isEmpty()) {
264 				// All constraints are valid for these nodes
265 				Node preferredData = null;
266 				if (dataProxies == null) {
267 					return getDatabaseObjectCard(cardModel, cachedInstances.get(0), true);
268 				}
269 				for (Proxy dataProxy : dataProxies) {
270 					for (Node node : validateCards) {
271 						if (dataProxy.getName().equals(node.getAttribute("proxy"))) {
272 							preferredData = node;
273 							break;
274 						}
275 					}
276 				}
277 
278 				if (preferredData == null) {
279 					// Do not retry a GET on a preferred proxy even if non ONE matches
280 					preferredData = validateCards.get(0);
281 				}
282 				if (constraints == null) {
283 					return getDatabaseObjectCard(cardModel, preferredData, true);
284 				}
285 				return getDatabaseObjectCard(cardModel, preferredData,
286 						bestScore >= constraints.size());
287 			}
288 			/*
289 			 * The card has been found in the cache but many constraints made failed
290 			 * the extraction. We try the proxy 'update' and if failed too, then
291 			 * return the first instance of cached data.
292 			 */
293 			// TODO try proxies first
294 			// return getDatabaseObjectCard(cardModel, cachedInstances.get(0));
295 		}
296 		// This card is not yet cached, no database object created
297 		return null;
298 	}
299 
300 	/***
301 	 * Retrieve data from the given proxy. If this card is not available throw
302 	 * this proxy, null value is returned.
303 	 * 
304 	 * @param proxy
305 	 *          the proxy retrieving data
306 	 * @param cardName
307 	 *          the card name (no translated one)
308 	 * @param constraints
309 	 *          constraints set{key,value}. May be null.
310 	 * @throws IOException
311 	 *           If some other I/O error occurs
312 	 * @return the built database object.
313 	 */
314 	private static DatabaseCard getDatabaseFromProxy(Proxy proxy,
315 			CardModel cardModel, Map<String, String> constraints) throws IOException {
316 		return proxy.getDatabaseFromStream(cardModel, constraints);
317 	}
318 
319 	private static DatabaseCard getDatabaseObjectCard(CardModel cardModel,
320 			Node preferredData, boolean consistent) {
321 		DatabaseCard databaseCard = databaseCardCache.get(preferredData);
322 		if (databaseCard != null) {
323 			// An existing instance has been found.
324 			databaseCard.updateCardModel(cardModel);
325 			return databaseCard;
326 		}
327 		cardModel.setLocalName(preferredData.getAttribute("local-name"));
328 		databaseCard = new DatabaseCard(cardModel, getProxy(preferredData
329 				.getAttribute("proxy")), pictureProxies);
330 		databaseCard.setConsistent(consistent);
331 		// Fill this new database object with the validated cached data
332 		if (preferredData.aList != null) {
333 			for (Object node : preferredData.aList) {
334 				if (node != null && node instanceof Node) {
335 					// Add this property to database object
336 					databaseCard.add(propertiesCacheConfig.get(
337 							((Node) node).getAttribute("name")).parseProperty(
338 							cardModel.getCardName(), (Node) node));
339 				}
340 			}
341 		}
342 		// Database object has been successfully filled, we save it in our cache
343 		databaseCardCache.put(preferredData, databaseCard);
344 		return databaseCard;
345 	}
346 
347 	/***
348 	 * Return the proxy object giving it's name.<code>null</code> value is
349 	 * returned if the proxy has not been found or if the <code>proxyName</code>
350 	 * was <code>null</code>, empty, or no proxy were loaded (occurs in
351 	 * initComponent() method calls).
352 	 * 
353 	 * @param proxyName
354 	 *          the poxy's name
355 	 * @return the proxy object if found, <code>null</code> otherwise.
356 	 */
357 	public static Proxy getProxy(String proxyName) {
358 		if (proxyName != null && proxyName.length() > 0 && dataProxies != null) {
359 			for (Proxy proxy : dataProxies) {
360 				if (proxy != null && proxyName.equals(proxy.getName())) {
361 					return proxy;
362 				}
363 			}
364 		}
365 		return null;
366 	}
367 
368 	/***
369 	 * Initialize property configurations from the dbStream. Existing proxies are
370 	 * also initialized.
371 	 * <ul>
372 	 * Structure of Stream : Data[size]
373 	 * <li>properties[] [...]</li>
374 	 * </ul>
375 	 * 
376 	 * @param dbStream
377 	 *          the MDB file containing rules
378 	 * @throws IOException
379 	 *           error during the database configuration.
380 	 */
381 	public static void init(InputStream dbStream) throws IOException {
382 		String[] dataProxyNameOrders = Configuration.getString(
383 				"database.dataOrder", "").split("//|");
384 		String[] pictureProxyNameOrders = Configuration.getString(
385 				"database.pictureOrder", "").split("//|");
386 		int count = dbStream.read();
387 		propertiesCacheConfig = new HashMap<String, PropertyConfig>(count);
388 		while (count-- > 0) {
389 			PropertyConfig propertyConfig = PropertyConfigFactory
390 					.getPropertyConfig(dbStream);
391 			propertiesCacheConfig.put(propertyConfig.getName(), propertyConfig);
392 		}
393 
394 		// list available proxies
395 		final File proxiesLocation = MToolKit.getTbsFile(IdConst.PROXIES_LOCATION);
396 		final File[] lproxies;
397 		if (proxiesLocation == null) {
398 			Log.warn("The proxy directory '"
399 					+ MToolKit.getTbsFile(IdConst.PROXIES_LOCATION, false)
400 					+ "' does not exist");
401 			lproxies = new File[0];
402 		} else {
403 			lproxies = proxiesLocation.listFiles(new FileFilterPlus("xml"));
404 		}
405 		dataProxies = new Proxy[lproxies.length];
406 		pictureProxies = new Proxy[lproxies.length];
407 
408 		XmlTools.initHashMaps();
409 
410 		// validate them and build 'Proxy' instances from XML
411 		for (int i = lproxies.length; i-- > 0;) {
412 			try {
413 				dataProxies[i] = new Proxy(lproxies[i]);
414 				pictureProxies[i] = dataProxies[i];
415 			} catch (Exception e) {
416 				e.printStackTrace();
417 			}
418 		}
419 
420 		// re-order proxies with the defined orders
421 		int index = 0;
422 		for (String dataProxyNameOrder : dataProxyNameOrders) {
423 			for (int j = 0; j < dataProxies.length; j++) {
424 				if (dataProxyNameOrder.equalsIgnoreCase(dataProxies[j].getXmlName())) {
425 					final Proxy oldProxy = dataProxies[index];
426 					dataProxies[index] = dataProxies[j];
427 					dataProxies[j] = oldProxy;
428 					index++;
429 					break;
430 				}
431 			}
432 		}
433 		index = 0;
434 		for (String pictureProxyNameOrder : pictureProxyNameOrders) {
435 			for (int j = 0; j < pictureProxies.length; j++) {
436 				if (pictureProxyNameOrder.equalsIgnoreCase(pictureProxies[j]
437 						.getXmlName())) {
438 					final Proxy oldProxy = pictureProxies[index];
439 					pictureProxies[index] = pictureProxies[j];
440 					pictureProxies[j] = oldProxy;
441 					index++;
442 					break;
443 				}
444 			}
445 		}
446 
447 		// Initialize source file accessibility
448 		try {
449 			sourceFile = FileImageSource.class.getDeclaredField("imagefile");
450 			sourceUrl = URLImageSource.class.getDeclaredField("url");
451 			AccessibleObject.setAccessible(new AccessibleObject[] { sourceFile },
452 					true);
453 			AccessibleObject
454 					.setAccessible(new AccessibleObject[] { sourceUrl }, true);
455 		} catch (Exception e) {
456 			e.printStackTrace();
457 		}
458 
459 		XmlTools.clean();
460 	}
461 
462 	/***
463 	 * Save the cache into the database XML file.
464 	 */
465 	public static void saveCache() {
466 		if (config != null) {
467 			try {
468 				PrintWriter printer = new PrintWriter(Configuration
469 						.loadTemplateTbsFile(IdConst.FILE_DATABASE_SAVED));
470 				printer.println(CACHE_FILE_HEADER);
471 				config.print(printer);
472 				IOUtils.closeQuietly(printer);
473 			} catch (IOException e) {
474 				// Cache could not be saved
475 				e.printStackTrace();
476 			}
477 		}
478 	}
479 
480 	/***
481 	 * Update the cache with the given data.
482 	 * 
483 	 * @param databaseCard
484 	 *          the data of a card to cache in our XML data base.
485 	 */
486 	private static void updateCache(DatabaseCard databaseCard) {
487 		if (config == null) {
488 			throw new IllegalStateException("Cache should be initialized");
489 		}
490 		databaseCard.updateCache(config);
491 	}
492 
493 	/***
494 	 * Prevent this class to be instantiated.
495 	 */
496 	private DatabaseFactory() {
497 		// Nothing to do
498 	}
499 }