001 /*
002 * Created by IntelliJ IDEA.
003 * User: d@hd.org
004 * Date: 09-Sep-02
005 * Time: 18:10:39
006
007 Copyright (c) 1996-2011, Damon Hart-Davis
008 All rights reserved.
009
010 Redistribution and use in source and binary forms, with or without
011 modification, are permitted provided that the following conditions are
012 met:
013
014 * Redistributions of source code must retain the above copyright
015 notice, this list of conditions and the following disclaimer.
016
017 * Redistributions in binary form must reproduce the above copyright
018 notice, this list of conditions and the following disclaimer in the
019 documentation and/or other materials provided with the
020 distribution.
021
022 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
023 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
024 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
025 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033
034 */
035 package org.hd.d.pg2k.svrCore;
036
037 import java.io.File;
038 import java.io.IOException;
039 import java.io.InvalidObjectException;
040 import java.io.ObjectInputValidation;
041 import java.io.Serializable;
042 import java.util.Collections;
043 import java.util.Date;
044 import java.util.Enumeration;
045 import java.util.HashMap;
046 import java.util.Iterator;
047 import java.util.List;
048 import java.util.Locale;
049 import java.util.Map;
050 import java.util.MissingResourceException;
051 import java.util.Properties;
052 import java.util.ResourceBundle;
053 import java.util.SortedMap;
054
055 import org.hd.d.pg2k.svrCore.AllExhibitPropertiesDelta.DiffException;
056 import org.hd.d.pg2k.svrCore.Tuple.Pair;
057 import org.hd.d.pg2k.svrCore.location.LocationMap;
058 import org.hd.d.pg2k.svrCore.props.PropertiesBundleDiff;
059 import org.hd.d.pg2k.svrCore.props.PropertiesDiff;
060
061 /**This contains global immutable exhibit data over all exhibits that must live with the exhibits.
062 * This is data that does not apply to any one exhibit but is small
063 * and critical to handling of all exhibits.
064 * <p>
065 * Currently this contains:
066 * <ul>
067 * <li>The AEP-attached location DB properties.
068 * <li>The prefix translations and comments and aliases (tree description).
069 * </ul>
070 * <p>
071 * This can include:
072 * <ul>
073 * <li>tree-structured (i18n-ed) info
074 * <li>hot/cold lists
075 * <li>see-also lists (links)
076 * <li>vote history
077 * </ul>
078 * <p>
079 * This has a hash computed over its contents to make it easy for users
080 * of this object to tell if it has changed since previous incarnations.
081 * <p>
082 * This object is immutable and Serializable, and tries to validate its
083 * contents upon deserialisation.
084 */
085 public final class ExhibitPropsGlobalImmutable implements Serializable
086 {
087 /**Create an empty instance. */
088 public ExhibitPropsGlobalImmutable()
089 { this((PropertiesDiff) null, 0, (PropertiesBundleDiff) null, 0); }
090
091 /**Create an instance. */
092 public ExhibitPropsGlobalImmutable(final Properties locationDB,
093 final long locationDBTimestamp,
094 final Map<String, Properties> treedesc,
095 final long treedescTimestamp)
096 {
097 this(PropertiesDiff.createAsStandAloneDiff(locationDB), locationDBTimestamp,
098 PropertiesBundleDiff.createAsStandAloneDiff(treedesc), treedescTimestamp);
099 }
100
101 /**Create an instance. */
102 public ExhibitPropsGlobalImmutable(final PropertiesDiff locationDB,
103 final long locationDBTimestamp,
104 final PropertiesBundleDiff treedesc,
105 final long treedescTimestamp)
106 {
107 this.locationDB = ((locationDB == null) || !locationDB.isEmpty()) ? locationDB : null;
108 this.locationDBTimestamp = locationDBTimestamp;
109 this.treedesc = ((treedesc == null) || !treedesc.isEmpty()) ? treedesc : null;
110 this.treedescTimestamp = treedescTimestamp;
111
112 // Compute a +ve composite hash.
113 // Zero if this instance contains no data.
114 longHash =
115 (((treedesc == null) ? 0 : (treedesc.hashCode() << 16)) ^
116 ((locationDB == null) ? 0 : 3*locationDB.hashCode())) >>> 1;
117
118 try { validateObject(); }
119 catch(final InvalidObjectException e) { throw new IllegalArgumentException(e); }
120 }
121
122 /**Load global data from filesystem.
123 */
124 public static ExhibitPropsGlobalImmutable loadFromDataDir(final File exhibitDataDir)
125 throws IOException
126 {
127 if(exhibitDataDir == null) { throw new IllegalArgumentException(); }
128
129 // Load locationDB.
130 final File locDBF = new File(exhibitDataDir, CoreConsts.LOCDB_PROPS_NAME + ".properties");
131 final Tuple.Pair<Properties, Long> locDB = PropertiesDiff.loadFromFile(locDBF);
132 // Load treedesc tree-structured i18n AKA/description info.
133 final Pair<Map<String, Properties>, Long> treedescBundle = PropertiesBundleDiff.loadBundle(new File(exhibitDataDir, "_i18n"), "treedesc");
134 return(new ExhibitPropsGlobalImmutable(
135 locDB.first, locDB.second.longValue(),
136 treedescBundle.first, treedescBundle.second.longValue()));
137 }
138
139 /**The location DB timestamp; strictly positive, or zero if no/empty locationDB. */
140 public final long locationDBTimestamp;
141
142 /**The (immutable) location DB in properties format; null if absent.
143 * We store this in PropertiesDiff format for efficiency on the wire,
144 * but convert it to LocationMap before use.
145 * <p>
146 * May be absent if there was no location data available at AEP creation
147 * or when deserialising an older AEP that has no location data,
148 * or as a more efficient way to represent an empty value
149 * (we always store an empty value as null for efficiency).
150 */
151 private final PropertiesDiff locationDB;
152
153 /**The treedesc bundle timestamp; non-negative, or may be zero if locationDBis empty/absent. */
154 public final long treedescTimestamp;
155
156 /**The (immutable) treedesc bundle; null if absent.
157 * We store this in PropertiesBundleDiff format for efficiency on the wire.
158 * <p>
159 * May be absent if there was no location data available at AEP creation
160 * or when deserialising an older AEP that has no location data,
161 * or as a more efficient way to represent an empty value
162 * (we always store an empty value as null for efficiency).
163 */
164 private final PropertiesBundleDiff treedesc;
165
166 /**Get the hash of the treedesc; non-negative, or may be zero if treedesc is empty/absent. */
167 public final int getTreedescHash()
168 { return((treedesc == null) ? 0 : treedesc.hashCode()); }
169
170 /**The (immutable) LocationMap derived from the locationDB; never null.
171 * Created on or before first use.
172 * <p>
173 * Derived from the locationDB info, empty if no locationDB.
174 * <p>
175 * Not part of the serialised state.
176 * <p>
177 * Marked volatile for lock-free thread-safe access.
178 */
179 private volatile transient LocationMap locMap;
180
181 /**Get the LocationMap; never null. */
182 public LocationMap getLocationMap()
183 {
184 // Once the value is computed, reads do not require a lock.
185 LocationMap result = locMap;
186 if(locMap != null) { return(locMap); }
187
188 // We prevent one than one thread creating the LocationMap instance.
189 synchronized(this)
190 {
191 // If another thread got in there first and did the work
192 // then we don't need to do it again.
193 result = locMap;
194 if(locMap != null) { return(locMap); }
195
196 try
197 {
198 // Compute and cache locMap for first access.
199 result = (locationDB == null) ? (new LocationMap()) :
200 (new LocationMap(PropertiesDiff.createFromStandAloneDiff(locationDB), locationDBTimestamp));
201 }
202 catch(final DiffException e)
203 {
204 // Should not happen.
205 // Data errors should have been caught at construction/deserialisation.
206 throw new Error(e);
207 }
208 locMap = result;
209 return(result);
210 }
211 }
212
213 private static final class RBControl extends ResourceBundle.Control
214 {
215 /**Wrap a control around the treedesc data. */
216 RBControl(final PropertiesBundleDiff treedesc)
217 { this.treedesc = treedesc; }
218
219 /**The treedesc bundle data; never null. */
220 private final PropertiesBundleDiff treedesc;
221
222 /**Claim a 'local' format, ie not properties or class. */
223 @Override
224 public List<String> getFormats(final String baseName)
225 { return(Collections.singletonList("local")); }
226
227 /**Wrap our local treedesc bundle to return; null if no such bundle available. */
228 @Override
229 public ResourceBundle newBundle(final String baseName,
230 final Locale locale,
231 final String format,
232 final ClassLoader loader,
233 final boolean reload)
234 {
235 if((baseName == null) || (locale == null))
236 { throw new IllegalArgumentException(); }
237 // Compute bundle name tail (empty basename) for lookup in PropertiesBundleDiff.
238 final String bundleName = toBundleName("", locale);
239 // If we have a treedesc bundle with the given name, wrap and return it.
240 final PropertiesDiff pd = treedesc.getNewValues().get(bundleName);
241 if(pd != null) { return(new PropertiesDiffResourceBundle(pd)); }
242 // If we have no such bundle then return null.
243 return(null);
244 }
245
246 /**Prevent cacheing in the ResourceBundle mechanism. */
247 @Override public boolean needsReload(final String baseName, final Locale locale, final String format, final ClassLoader loader, final ResourceBundle bundle, final long loadTime)
248 { return(true); }
249
250 /**Prevent cacheing in the ResourceBundle mechanism. */
251 @Override public long getTimeToLive(final String baseName, final Locale locale)
252 { return(TTL_DONT_CACHE); }
253 }
254
255
256 /**Thin wrapper/holder for a local treedesc resource bundle based on PropertiesDiff.
257 * This returns entries out of the "new" data.
258 * @author dhd
259 */
260 private static final class PropertiesDiffResourceBundle extends ResourceBundle
261 {
262 /**Handle on the underlying (immutable) data; never null. */
263 private final SortedMap<CharSequence, CharSequence> map;
264
265 /**Construct with (non-null) underlying data. */
266 PropertiesDiffResourceBundle(final PropertiesDiff pd) { map = pd.getNewValues(); }
267
268 @Override
269 protected Object handleGetObject(final String key) { return(map.get(key)); }
270 @Override
271 public Enumeration<String> getKeys()
272 {
273 return(new Enumeration<String>() {
274 final Iterator<CharSequence> i = map.keySet().iterator();
275 public boolean hasMoreElements() { return(i.hasNext()); }
276 public String nextElement() { return(i.next().toString()); }
277 });
278 }
279 }
280
281 /**Gets the appropriate localised tree-description message; never null unless the key is null.
282 * In case of a missing message, or unavailable treedesc data,
283 * this returns the key itself.
284 * <p>
285 * The key is converted to lower-case (if necessary) before lookup.
286 * <p>
287 * If the name is null, the result is null.
288 *
289 * @param locale desired locale, or null
290 */
291 public CharSequence getLocalisedTreeDescMessage(final String msgName,
292 final Locale locale)
293 {
294 if(msgName == null) { return(null); }
295
296 // If no treedesc data is available then return the original message.
297 if(treedesc == null) { return(msgName); }
298
299 // Create a control to guide the lookup in our global treedesc properties.
300 // This also prevents cacheing by the ResourceBundle mechanism,
301 // to avoid getting out of sync after a new AEP is loaded.
302 final ResourceBundle.Control ctrl = new RBControl(treedesc);
303
304 // Do the lookup in our local data via the system manager...
305 // We do this to get it to look up by partial locale in the usual order.
306 final ResourceBundle rb = ResourceBundle.getBundle(I18NTools.BUNDLE_TREEDESC, locale, ctrl);
307 // try { return(rb.getString(msgName)); }
308 try { return((CharSequence) rb.getObject(msgName)); }
309 // If requested value not available then return the original message.
310 catch(final MissingResourceException e) { return(msgName); }
311 }
312
313 /**The hash of all the data held; guaranteed non-negative.
314 * Depends all the information held in this object.
315 * <p>
316 * Will be zero if this contains no data.
317 */
318 public final long longHash;
319
320 /**Returns a hash code value for the object; derived from the longHash.
321 * Guaranteed zero if there are no exhibits.
322 *
323 * @return a hash code value for this object.
324 */
325 @Override
326 public int hashCode()
327 { return((int) longHash); }
328
329 /**Indicates whether some other object is "equal to" this one; the underlying data is the same if true.
330 *
331 * @param obj the reference object with which to compare.
332 * @return <code>true</code> if this object is the same as the obj
333 * argument; <code>false</code> otherwise.
334 */
335 @Override
336 public boolean equals(final Object obj)
337 {
338 if(this == obj) { return(true); }
339 // Must be of the same type to be equal.
340 if(!(obj instanceof ExhibitPropsGlobalImmutable)) { return(false); }
341 final ExhibitPropsGlobalImmutable other = (ExhibitPropsGlobalImmutable) obj;
342
343 // Hash calaculation may change wih newer EPGI implementations,
344 // so we can't use the longHash as a proxy for the content,
345 // unless we force its recomputation with readResolve().
346 if(longHash != other.longHash) { return(false); }
347
348 // Check the underlying immutable data.
349
350 // Location DB timestamps must match.
351 if(locationDBTimestamp != other.locationDBTimestamp) { return(false); }
352
353 // We check the locationDB value directly.
354 if(locationDB == null) { if(other.locationDB != null) { return(false); } }
355 else if(!locationDB.equals(other.locationDB)) { return(false); }
356
357 // Treedesc timestamps must match.
358 if(treedescTimestamp != other.treedescTimestamp) { return(false); }
359
360 // We check the locationDB value directly.
361 if(treedesc == null) { if(other.treedesc != null) { return(false); } }
362 else if(!treedesc.equals(other.treedesc)) { return(false); }
363
364 return(true); // Equal.
365 }
366
367 /**Human-readable summary of state. */
368 @Override public String toString()
369 {
370 final StringBuilder result = new StringBuilder(128);
371 result.append("EPGI");
372 result.append(":locationDB[").append(locationDB). /* append('|').append(new Date(locationDBTimestamp)). */ append(']');
373 result.append(":treedesc[").append(treedesc). /* append('|').append(new Date(treedescTimestamp)). */ append(']');
374 return(result.toString());
375 }
376
377 /**Validate fields/state.
378 * Called in the constructor and possibly after de-serialising.
379 * <p>
380 * Barf if something bad is found.
381 * (Maybe allow some extra info in debug version.)
382 */
383 public void validateObject()
384 throws InvalidObjectException
385 {
386 // Check that all components are sane and safe.
387 if(longHash < 0)
388 { throw new InvalidObjectException("bad object: longHash < 0"); }
389 if(locationDBTimestamp < 0)
390 { throw new InvalidObjectException("bad object: locationDBTimestamp < 0"); }
391 if((locationDBTimestamp == 0) && (locationDB != null))
392 { throw new InvalidObjectException("bad object: locationDBTimestamp does not match locationDB presence: "+(new Date(locationDBTimestamp))+" vs "+locationDB); }
393 if(treedescTimestamp < 0)
394 { throw new InvalidObjectException("bad object: treedescTimestamp < 0"); }
395 if((treedescTimestamp == 0) && (treedesc != null))
396 { throw new InvalidObjectException("bad object: treedescTimestamp does not match treedesc presence: "+(new Date(treedescTimestamp))+" vs "+treedesc); }
397 }
398
399 /**Deserialise: use constructor for validation, defensive copying, conversion from old formats, etc.
400 * Also allows us to (re)normalise the data in this instance,
401 * eg to recompute the longHash in case our hash algorithm changes.
402 */
403 protected Object readResolve()
404 throws java.io.ObjectStreamException
405 {
406 return(new ExhibitPropsGlobalImmutable(locationDB,
407 locationDBTimestamp,
408 treedesc,
409 treedescTimestamp));
410 }
411
412 /**Our serial version... */
413 private static final long serialVersionUID = 0xf130cee4509ca258L;
414
415
416 /**Immutable record of the difference between two EPGI instances.
417 * This contains the "diffable" items.
418 */
419 public static final class EPGIDiff implements Serializable, ObjectInputValidation
420 {
421 /**Create an empty instance. */
422 public EPGIDiff()
423 { this(0, null, 0, null); }
424
425 /**Create an instance. */
426 public EPGIDiff(final long locationDBTimestampNew,
427 final PropertiesDiff locationDBDiff,
428 final long treedescTimestampNew,
429 final PropertiesBundleDiff treedescDiff)
430 {
431 this.locationDBTimestampNew = locationDBTimestampNew;
432 this.locationDBDiff = ((locationDBDiff == null) || !locationDBDiff.isEmpty()) ? locationDBDiff : null;
433 this.treedescTimestampNew = treedescTimestampNew;
434 this.treedescDiff = ((treedescDiff == null) || !treedescDiff.isEmpty()) ? treedescDiff : null;
435 try { validateObject(); }
436 catch(final InvalidObjectException e) { throw new IllegalArgumentException(e); }
437 }
438
439 /**The new location DB timestamp; strictly positive, or zero for empty diff. */
440 public final long locationDBTimestampNew;
441
442 /**The diffed location DB in properties format; non-empty else null. */
443 private final PropertiesDiff locationDBDiff;
444
445 /**The new treedesc timestamp; strictly positive, or zero for empty diff. */
446 public final long treedescTimestampNew;
447
448 /**The diffed treedesc in properties format; non-empty else null. */
449 private final PropertiesBundleDiff treedescDiff;
450
451 /**Create a diff from the old EPGI value to the new one.
452 * If the new locationDB is absent then we fake a timestamp of now.
453 */
454 public static EPGIDiff createDiff(final ExhibitPropsGlobalImmutable e1,
455 final ExhibitPropsGlobalImmutable e2)
456 throws DiffException
457 {
458 if((null == e1) || (null == e2))
459 { throw new IllegalArgumentException(); }
460 return(new EPGIDiff(
461 e2.locationDBTimestamp,
462 PropertiesDiff.createDiff(
463 PropertiesDiff.createFromStandAloneDiff(e1.locationDB != null ? e1.locationDB : PropertiesDiff.EMPTY_DIFF),
464 PropertiesDiff.createFromStandAloneDiff(e2.locationDB != null ? e2.locationDB : PropertiesDiff.EMPTY_DIFF),
465 true, // We always need a diff.
466 false), // Should already be intern()ed by now.
467 e2.treedescTimestamp,
468 PropertiesBundleDiff.createDiff(
469 PropertiesBundleDiff.createFromStandAloneDiff(e1.treedesc != null ? e1.treedesc : PropertiesBundleDiff.EMPTY_DIFF),
470 PropertiesBundleDiff.createFromStandAloneDiff(e2.treedesc != null ? e2.treedesc : PropertiesBundleDiff.EMPTY_DIFF),
471 true, // We always need a diff.
472 false))); // Should already be intern()ed by now.
473 }
474
475 /**Apply diff to derive new EPGI; never null.
476 * @param oldEpgi null is treated as if an empty EPGI
477 * @param diff diff to apply; never null
478 * @return new non-null EPGI value from old one (null implies empty) and diff
479 */
480 public static ExhibitPropsGlobalImmutable applyDiff(ExhibitPropsGlobalImmutable oldEpgi,
481 final EPGIDiff diff)
482 {
483 if(oldEpgi == null) { oldEpgi = new ExhibitPropsGlobalImmutable(); }
484 if(diff == null) { throw new IllegalArgumentException(); }
485 try
486 {
487 return(new ExhibitPropsGlobalImmutable(
488 PropertiesDiff.createAsStandAloneDiff(
489 PropertiesDiff.applyDiff((oldEpgi.locationDB == null) ? new Properties() : PropertiesDiff.createFromStandAloneDiff(oldEpgi.locationDB),
490 (diff.locationDBDiff != null) ? diff.locationDBDiff : PropertiesDiff.EMPTY_DIFF)),
491 diff.locationDBTimestampNew,
492 PropertiesBundleDiff.createAsStandAloneDiff(
493 PropertiesBundleDiff.applyDiff((oldEpgi.treedesc == null) ? (new HashMap<String,Properties>()) : PropertiesBundleDiff.createFromStandAloneDiff(oldEpgi.treedesc),
494 (diff.treedescDiff != null) ? diff.treedescDiff : PropertiesBundleDiff.EMPTY_DIFF)),
495 diff.treedescTimestampNew));
496 }
497 catch(final DiffException e)
498 { throw new Error(e); /* Should never happen. */ }
499 }
500
501 /**Deserialise: use constructor for validation, defensive copying, conversion from old formats, etc.
502 * Also allows us to (re)normalise the data in this instance.
503 */
504 protected Object readResolve()
505 throws java.io.ObjectStreamException
506 {
507 return(new EPGIDiff(locationDBTimestampNew,
508 locationDBDiff,
509 treedescTimestampNew,
510 treedescDiff));
511 }
512
513 /**Checks that the object is internally consistent. */
514 public void validateObject() throws InvalidObjectException
515 {
516 if((locationDBTimestampNew == 0) != (locationDBDiff == null))
517 { throw new InvalidObjectException("bad object: zero locationDBTimestampNew <=> empty diff"); }
518 if(locationDBTimestampNew < 0)
519 { throw new InvalidObjectException("bad object: -ve locationDBTimestampNew"); }
520 if((locationDBDiff != null) && locationDBDiff.isEmpty())
521 { throw new InvalidObjectException("bad object: non-null empty locationDBDiff"); }
522
523 if((treedescTimestampNew == 0) != (treedescDiff == null))
524 { throw new InvalidObjectException("bad object: zero treedescTimestampNew <=> empty diff"); }
525 if(treedescTimestampNew < 0)
526 { throw new InvalidObjectException("bad object: -ve treedescTimestampNew"); }
527 if((treedescDiff != null) && treedescDiff.isEmpty())
528 { throw new InvalidObjectException("bad object: non-null empty treedescDiff"); }
529 }
530
531 /**Serial UID.*/
532 private static final long serialVersionUID = -3566302089418451668L;
533 }
534 }