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        }