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-2012, 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.Serializable;
041 import java.util.Collections;
042 import java.util.Date;
043 import java.util.Enumeration;
044 import java.util.Iterator;
045 import java.util.List;
046 import java.util.Locale;
047 import java.util.Map;
048 import java.util.MissingResourceException;
049 import java.util.Properties;
050 import java.util.ResourceBundle;
051 import java.util.SortedMap;
052
053 import org.hd.d.pg2k.svrCore.AllExhibitPropertiesDelta.DiffException;
054 import org.hd.d.pg2k.svrCore.Tuple.Pair;
055 import org.hd.d.pg2k.svrCore.location.LocationMap;
056 import org.hd.d.pg2k.svrCore.props.PropertiesBundleDiff;
057 import org.hd.d.pg2k.svrCore.props.PropertiesDiff;
058
059 /**This contains global immutable exhibit data over all exhibits that must live with the exhibits.
060 * This is data that does not apply to any one exhibit but is small
061 * and critical to handling of all exhibits.
062 * <p>
063 * Currently this contains:
064 * <ul>
065 * <li>The AEP-attached location DB properties.
066 * <li>The prefix translations and comments and aliases (tree description).
067 * </ul>
068 * <p>
069 * This can include:
070 * <ul>
071 * <li>tree-structured (i18n-ed) info
072 * <li>hot/cold lists
073 * <li>see-also lists (links)
074 * <li>vote history
075 * </ul>
076 * <p>
077 * This has a hash computed over its contents to make it easy for users
078 * of this object to tell if it has changed since previous incarnations.
079 * <p>
080 * This object is immutable and Serializable, and tries to validate its
081 * contents upon deserialisation.
082 */
083 public final class ExhibitPropsGlobalImmutable implements Serializable
084 {
085 /**Create an empty instance. */
086 public ExhibitPropsGlobalImmutable()
087 { this((PropertiesDiff) null, 0, (PropertiesBundleDiff) null, 0); }
088
089 /**Create an instance. */
090 public ExhibitPropsGlobalImmutable(final Properties locationDB,
091 final long locationDBTimestamp,
092 final Map<String, Properties> treedesc,
093 final long treedescTimestamp)
094 {
095 this(PropertiesDiff.createAsStandAloneDiff(locationDB), locationDBTimestamp,
096 PropertiesBundleDiff.createAsStandAloneDiff(treedesc), treedescTimestamp);
097 }
098
099 /**Create an instance. */
100 public ExhibitPropsGlobalImmutable(final PropertiesDiff locationDB,
101 final long locationDBTimestamp,
102 final PropertiesBundleDiff treedesc,
103 final long treedescTimestamp)
104 {
105 this.locationDB = ((locationDB == null) || !locationDB.isEmpty()) ? locationDB : null;
106 this.locationDBTimestamp = locationDBTimestamp;
107 this.treedesc = ((treedesc == null) || !treedesc.isEmpty()) ? treedesc : null;
108 this.treedescTimestamp = treedescTimestamp;
109
110 // Compute a +ve composite hash.
111 // Zero if this instance contains no data.
112 longHash =
113 (((treedesc == null) ? 0 : (treedesc.hashCode() << 16)) ^
114 ((locationDB == null) ? 0 : 3*locationDB.hashCode())) >>> 1;
115
116 try { validateObject(); }
117 catch(final InvalidObjectException e) { throw new IllegalArgumentException(e); }
118 }
119
120 /**Load global data from filesystem. */
121 public static ExhibitPropsGlobalImmutable loadFromDataDir(final File exhibitDataDir)
122 throws IOException
123 {
124 if(exhibitDataDir == null) { throw new IllegalArgumentException(); }
125
126 // Load locationDB.
127 final File locDBF = new File(exhibitDataDir, CoreConsts.LOCDB_PROPS_NAME + ".properties");
128 final Tuple.Pair<Properties, Long> locDB = PropertiesDiff.loadFromFile(locDBF);
129 // Load treedesc tree-structured i18n AKA/description info.
130 final Pair<Map<String, Properties>, Long> treedescBundle = PropertiesBundleDiff.loadBundle(new File(exhibitDataDir, "_i18n"), "treedesc");
131 return(new ExhibitPropsGlobalImmutable(
132 locDB.first, locDB.second.longValue(),
133 treedescBundle.first, treedescBundle.second.longValue()));
134 }
135
136 /**The location DB timestamp; strictly positive, or zero if no/empty locationDB. */
137 public final long locationDBTimestamp;
138
139 /**The (immutable) location DB in properties format; null if absent.
140 * We store this in PropertiesDiff format for efficiency on the wire,
141 * but convert it to LocationMap before use.
142 * <p>
143 * May be absent if there was no location data available at AEP creation
144 * or when deserialising an older AEP that has no location data,
145 * or as a more efficient way to represent an empty value
146 * (we always store an empty value as null for efficiency).
147 */
148 final PropertiesDiff locationDB;
149
150 /**The treedesc bundle timestamp; non-negative, or may be zero if locationDBis empty/absent. */
151 public final long treedescTimestamp;
152
153 /**The (immutable) treedesc bundle; null if absent.
154 * We store this in PropertiesBundleDiff format for efficiency on the wire.
155 * <p>
156 * May be absent if there was no location data available at AEP creation
157 * or when deserialising an older AEP that has no location data,
158 * or as a more efficient way to represent an empty value
159 * (we always store an empty value as null for efficiency).
160 */
161 final PropertiesBundleDiff treedesc;
162
163 /**Get the hash of the treedesc; non-negative, or may be zero if treedesc is empty/absent. */
164 public final int getTreedescHash()
165 { return((treedesc == null) ? 0 : treedesc.hashCode()); }
166
167 /**The (immutable) LocationMap derived from the locationDB; never null.
168 * Created on or before first use.
169 * <p>
170 * Derived from the locationDB info, empty if no locationDB.
171 * <p>
172 * Not part of the serialised state.
173 * <p>
174 * Marked volatile for lock-free thread-safe access.
175 */
176 private volatile transient LocationMap locMap;
177
178 /**Get the LocationMap; never null. */
179 public LocationMap getLocationMap()
180 {
181 // Once the value is computed, reads do not require a lock.
182 LocationMap result = locMap;
183 if(locMap != null) { return(locMap); }
184
185 // We prevent one than one thread creating the LocationMap instance.
186 synchronized(this)
187 {
188 // If another thread got in there first and did the work
189 // then we don't need to do it again.
190 result = locMap;
191 if(locMap != null) { return(locMap); }
192
193 try
194 {
195 // Compute and cache locMap for first access.
196 result = (locationDB == null) ? (new LocationMap()) :
197 (new LocationMap(PropertiesDiff.createFromStandAloneDiff(locationDB), locationDBTimestamp));
198 }
199 catch(final DiffException e)
200 {
201 // Should not happen.
202 // Data errors should have been caught at construction/deserialisation.
203 throw new Error(e);
204 }
205 locMap = result;
206 return(result);
207 }
208 }
209
210 private static final class RBControl extends ResourceBundle.Control
211 {
212 /**Wrap a control around the treedesc data. */
213 RBControl(final PropertiesBundleDiff treedesc)
214 { this.treedesc = treedesc; }
215
216 /**The treedesc bundle data; never null. */
217 private final PropertiesBundleDiff treedesc;
218
219 /**Claim a 'local' format, ie not properties or class. */
220 @Override
221 public List<String> getFormats(final String baseName)
222 { return(Collections.singletonList("local")); }
223
224 /**Wrap our local treedesc bundle to return; null if no such bundle available. */
225 @Override
226 public ResourceBundle newBundle(final String baseName,
227 final Locale locale,
228 final String format,
229 final ClassLoader loader,
230 final boolean reload)
231 {
232 if((baseName == null) || (locale == null))
233 { throw new IllegalArgumentException(); }
234 // Compute bundle name tail (empty basename) for lookup in PropertiesBundleDiff.
235 final String bundleName = toBundleName("", locale);
236 // If we have a treedesc bundle with the given name, wrap and return it.
237 final PropertiesDiff pd = treedesc.getNewValues().get(bundleName);
238 if(pd != null) { return(new PropertiesDiffResourceBundle(pd)); }
239 // If we have no such bundle then return null.
240 return(null);
241 }
242
243 /**Prevent cacheing in the ResourceBundle mechanism. */
244 @Override public boolean needsReload(final String baseName, final Locale locale, final String format, final ClassLoader loader, final ResourceBundle bundle, final long loadTime)
245 { return(true); }
246
247 /**Prevent cacheing in the ResourceBundle mechanism. */
248 @Override public long getTimeToLive(final String baseName, final Locale locale)
249 { return(TTL_DONT_CACHE); }
250 }
251
252
253 /**Thin wrapper/holder for a local treedesc resource bundle based on PropertiesDiff.
254 * This returns entries out of the "new" data.
255 * @author dhd
256 */
257 private static final class PropertiesDiffResourceBundle extends ResourceBundle
258 {
259 /**Handle on the underlying (immutable) data; never null. */
260 private final SortedMap<CharSequence, CharSequence> map;
261
262 /**Construct with (non-null) underlying data. */
263 PropertiesDiffResourceBundle(final PropertiesDiff pd) { map = pd.getNewValues(); }
264
265 @Override
266 protected Object handleGetObject(final String key) { return(map.get(key)); }
267 @Override
268 public Enumeration<String> getKeys()
269 {
270 return(new Enumeration<String>() {
271 final Iterator<CharSequence> i = map.keySet().iterator();
272 public boolean hasMoreElements() { return(i.hasNext()); }
273 public String nextElement() { return(i.next().toString()); }
274 });
275 }
276 }
277
278 /**Gets the appropriate localised tree-description message; never null unless the key is null.
279 * In case of a missing message, or unavailable treedesc data,
280 * this returns the key itself.
281 * <p>
282 * The key is converted to lower-case (if necessary) before lookup.
283 * <p>
284 * If the name is null, the result is null.
285 *
286 * @param locale desired locale, or null
287 */
288 public CharSequence getLocalisedTreeDescMessage(final String msgName,
289 final Locale locale)
290 {
291 if(msgName == null) { return(null); }
292
293 // If no treedesc data is available then return the original message.
294 if(treedesc == null) { return(msgName); }
295
296 // Create a control to guide the lookup in our global treedesc properties.
297 // This also prevents cacheing by the ResourceBundle mechanism,
298 // to avoid getting out of sync after a new AEP is loaded.
299 final ResourceBundle.Control ctrl = new RBControl(treedesc);
300
301 // Do the lookup in our local data via the system manager...
302 // We do this to get it to look up by partial locale in the usual order.
303 final ResourceBundle rb = ResourceBundle.getBundle(I18NTools.BUNDLE_TREEDESC, locale, ctrl);
304 // try { return(rb.getString(msgName)); }
305 try { return((CharSequence) rb.getObject(msgName)); }
306 // If requested value not available then return the original message.
307 catch(final MissingResourceException e) { return(msgName); }
308 }
309
310 /**The hash of all the data held; guaranteed non-negative.
311 * Depends all the information held in this object.
312 * <p>
313 * Will be zero if this contains no data.
314 */
315 public final long longHash;
316
317 /**Returns a hash code value for the object; derived from the longHash.
318 * Guaranteed zero if there are no exhibits.
319 *
320 * @return a hash code value for this object.
321 */
322 @Override
323 public int hashCode()
324 { return((int) longHash); }
325
326 /**Indicates whether some other object is "equal to" this one; the underlying data is the same if true.
327 *
328 * @param obj the reference object with which to compare.
329 * @return <code>true</code> if this object is the same as the obj
330 * argument; <code>false</code> otherwise.
331 */
332 @Override
333 public boolean equals(final Object obj)
334 {
335 if(this == obj) { return(true); }
336 // Must be of the same type to be equal.
337 if(!(obj instanceof ExhibitPropsGlobalImmutable)) { return(false); }
338 final ExhibitPropsGlobalImmutable other = (ExhibitPropsGlobalImmutable) obj;
339
340 // Hash calaculation may change wih newer EPGI implementations,
341 // so we can't use the longHash as a proxy for the content,
342 // unless we force its recomputation with readResolve().
343 if(longHash != other.longHash) { return(false); }
344
345 // Check the underlying immutable data.
346
347 // Location DB timestamps must match.
348 if(locationDBTimestamp != other.locationDBTimestamp) { return(false); }
349
350 // We check the locationDB value directly.
351 if(locationDB == null) { if(other.locationDB != null) { return(false); } }
352 else if(!locationDB.equals(other.locationDB)) { return(false); }
353
354 // Treedesc timestamps must match.
355 if(treedescTimestamp != other.treedescTimestamp) { return(false); }
356
357 // We check the locationDB value directly.
358 if(treedesc == null) { if(other.treedesc != null) { return(false); } }
359 else if(!treedesc.equals(other.treedesc)) { return(false); }
360
361 return(true); // Equal.
362 }
363
364 /**Human-readable summary of state. */
365 @Override public String toString()
366 {
367 final StringBuilder result = new StringBuilder(128);
368 result.append("EPGI");
369 result.append(":locationDB[").append(locationDB). /* append('|').append(new Date(locationDBTimestamp)). */ append(']');
370 result.append(":treedesc[").append(treedesc). /* append('|').append(new Date(treedescTimestamp)). */ append(']');
371 return(result.toString());
372 }
373
374 /**Validate fields/state.
375 * Called in the constructor and possibly after de-serialising.
376 * <p>
377 * Barf if something bad is found.
378 * (Maybe allow some extra info in debug version.)
379 */
380 public void validateObject()
381 throws InvalidObjectException
382 {
383 // Check that all components are sane and safe.
384 if(longHash < 0)
385 { throw new InvalidObjectException("bad object: longHash < 0"); }
386 if(locationDBTimestamp < 0)
387 { throw new InvalidObjectException("bad object: locationDBTimestamp < 0"); }
388 if((locationDBTimestamp == 0) && (locationDB != null))
389 { throw new InvalidObjectException("bad object: locationDBTimestamp does not match locationDB presence: "+(new Date(locationDBTimestamp))+" vs "+locationDB); }
390 if(treedescTimestamp < 0)
391 { throw new InvalidObjectException("bad object: treedescTimestamp < 0"); }
392 if((treedescTimestamp == 0) && (treedesc != null))
393 { throw new InvalidObjectException("bad object: treedescTimestamp does not match treedesc presence: "+(new Date(treedescTimestamp))+" vs "+treedesc); }
394 }
395
396 /**Deserialise: use constructor for validation, defensive copying, conversion from old formats, etc.
397 * Also allows us to (re)normalise the data in this instance,
398 * eg to recompute the longHash in case our hash algorithm changes.
399 */
400 protected Object readResolve()
401 throws java.io.ObjectStreamException
402 {
403 return(new ExhibitPropsGlobalImmutable(locationDB,
404 locationDBTimestamp,
405 treedesc,
406 treedescTimestamp));
407 }
408
409 /**Our serial version... */
410 private static final long serialVersionUID = 0xf130cee4509ca258L;
411 }