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        }