View Javadoc

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