001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    package org.hd.d.pg2k.svrCore.props;
030    
031    import java.io.File;
032    import java.io.FileNotFoundException;
033    import java.io.FilenameFilter;
034    import java.io.IOException;
035    import java.io.InvalidObjectException;
036    import java.io.ObjectInputStream;
037    import java.io.ObjectInputValidation;
038    import java.io.ObjectOutputStream;
039    import java.io.Serializable;
040    import java.util.Collections;
041    import java.util.HashMap;
042    import java.util.HashSet;
043    import java.util.Iterator;
044    import java.util.Map;
045    import java.util.Properties;
046    import java.util.Set;
047    import java.util.SortedMap;
048    import java.util.SortedSet;
049    import java.util.TreeMap;
050    import java.util.TreeSet;
051    
052    import org.hd.d.pg2k.svrCore.AllExhibitPropertiesDelta.DiffException;
053    import org.hd.d.pg2k.svrCore.MemoryTools;
054    import org.hd.d.pg2k.svrCore.Tuple;
055    import org.hd.d.pg2k.svrCore.Tuple.Pair;
056    
057    /**A diff/patch mechanism for a bundle of well-behaved simple String-->String Properties.
058     * These diffs can be used to send just changes/deltas over the wire
059     * for well-behaved bundles of closely-related pure-String Properties,
060     * eg of the form of i18n ResourceBundle inputs.
061     * <p>
062     * This class itself is immutable and Serialisable,
063     * and designed to be efficient on the wire.
064     * <p>
065     * This uses PropertiesDiff to hold each Properties instance immutably/efficiently.
066     *
067     * @author DHD
068     */
069    public class PropertiesBundleDiff implements Serializable, ObjectInputValidation
070        {
071        /**The immutable in-order set of name (eg locales) of Properties sets entirely removed; never null.
072         * This field is NOT serialised directly.
073         */
074        private transient /*final*/ SortedSet<String> deletedNames;
075    
076        /**The immutable in-order map from items/names (eg locales) to the new/changed Properties sets; never null.
077         * Note that we store the differences in each underlying Properties set
078         * to efficiently represent small changes within each set.
079         * <p>
080         * This field is NOT serialised directly.
081         */
082        private transient /*final*/ SortedMap<String, PropertiesDiff> newValues;
083    
084        /**The input bundle count as a sanity-check; guaranteed non-negative. */
085        public final int sizeBefore;
086    
087        /**The output bundle count  as a sanity-check; guaranteed non-negative. */
088        public final int sizeAfter;
089    
090        /**Immutable empty diff. */
091        public static final PropertiesBundleDiff EMPTY_DIFF = new PropertiesBundleDiff();
092    
093        /**Make new empty diff instance. */
094        public PropertiesBundleDiff()
095            {
096            deletedNames = Collections.unmodifiableSortedSet(new TreeSet<String>());
097            newValues = Collections.unmodifiableSortedMap(new TreeMap<String,PropertiesDiff>());
098            sizeBefore = 0;
099            sizeAfter = 0;
100            // We assume that this does not need run-time validation with validateObject().
101            }
102    
103        /**Create diff instance.
104         * We defensively copy the potentially-mutable Collection argument.
105         * @param sizeBefore  size of input Properties; non-negative
106         * @param sizeAfter   size of output Properties; non-negative
107         * @param deleted     named Properties sets present in input and not present in output; non-null
108         * @param added       new/changed named Properties sets in output; non-null
109         */
110        public PropertiesBundleDiff(final int sizeBefore, final int sizeAfter,
111                                    final Set<String> deleted,
112                                    final Map<String,PropertiesDiff> added)
113            {
114            deletedNames = Collections.unmodifiableSortedSet(new TreeSet<String>(deleted));
115            newValues = Collections.unmodifiableSortedMap(new TreeMap<String,PropertiesDiff>(added));
116            this.sizeBefore = sizeBefore;
117            this.sizeAfter = sizeAfter;
118            try { validateObject(); }
119            catch(final InvalidObjectException e) { throw new IllegalArgumentException(e); }
120            }
121    
122        /**Get newValues; never null. */
123        public SortedMap<String, PropertiesDiff> getNewValues()
124            { return(newValues); }
125    
126        /**Create "change" values from a Properties bundle; never null.
127         * This is the set of values to be added to an empty Properties value
128         * to regenerate this one.
129         * <p>
130         * The first element in each Pair is the (non-null) Properties set name;
131         * the second element is the (non-null) Properties set.
132         *
133         * @param pb non-null pure String-->String properties set
134         */
135        private static Set<Tuple.Pair<String,Properties>> _computeChangeValues(final Map<String,Properties> pb)
136            {
137            final Set<Tuple.Pair<String,Properties>> result = new HashSet<Tuple.Pair<String,Properties>>(2*pb.size());
138            for(final Map.Entry entry : pb.entrySet())
139                {
140                result.add(new Tuple.Pair<String,Properties>((String) entry.getKey(),
141                                                             (Properties) entry.getValue()));
142                }
143            assert(result.size() == pb.size());
144            return(result);
145            }
146    
147        /**Private empty Properties map; never changed, never null. */
148        private static final Map<String,Properties> EMPTY_BUNDLE = Collections.emptyMap();
149    
150        /**Create diff against empty Properties.
151         * This is a safe, efficient, immutable, Serializable representation
152         * for a single Properties instance,
153         * where a raw Properties instance may not be safe/robust enough.
154         * <p>
155         * Forces intern()ing of the keys and values.
156         */
157        public static PropertiesBundleDiff createAsStandAloneDiff(final Map<String,Properties> p1)
158            {
159            try { return(createDiff(EMPTY_BUNDLE, p1, true, true)); }
160            catch(final DiffException e) { throw new Error(e); } // Should never happen.
161            }
162    
163        /**Create from diff against empty Properties.
164         * Recovers Properties instance created by createAsDiff.
165         */
166        public static Map<String,Properties> createFromStandAloneDiff(final PropertiesBundleDiff pd)
167            throws DiffException
168            { return(applyDiff(EMPTY_BUNDLE, pd)); }
169    
170        /**Private empty Properties map; never changed, never null. */
171        private static final Properties EMPTY_PROPERTIES = new Properties();
172    
173        /**Create diff between two Properties instances.
174         * This creates a diff to be applied to an instance of the first argument
175         * to recreate the second argument.
176         * <p>
177         * This will refuse to create a diff if it seems that
178         * the diff is unlikely to be useful, for example:
179         * <ul>
180         * <li>If one properties set has more than twice the entries of the other
181         *     (the diff may not be smaller than sending the second properties set whole).
182         * </ul>
183         *
184         * @param force  if true then force a diff to be produced
185         *     even if this routine would normally refuse to do so on efficiency grounds
186         * @param intern  if true than force intern()ing of all String values
187         *
188         * @throws DiffException  if no diff can be generated
189         *     or is unlikely to be worthwhile to use
190         *     (eg would be more than a fraction of the size of the second AEP)
191         */
192        public static PropertiesBundleDiff createDiff(final Map<String,Properties> p1,
193                                                      final Map<String,Properties> p2,
194                                                      final boolean force,
195                                                      final boolean intern)
196            throws DiffException
197            {
198            if((null == p1) || (null == p2))
199                { throw new IllegalArgumentException(); }
200    
201            if(!force)
202                {
203                if((p1.size() > 2*p2.size()) ||
204                   (p2.size() > 2*p1.size()))
205                    { throw new DiffException("sizes too different (force==false)"); }
206                }
207    
208            final Set<Tuple.Pair<String,Properties>> p1c = _computeChangeValues(p1);
209            final Set<Tuple.Pair<String,Properties>> p2c = _computeChangeValues(p2);
210    
211            // The items that we will need to have in our "added" list.
212            final Set<Tuple.Pair<String,Properties>> added = new HashSet<Tuple.Pair<String,Properties>>(p2c);
213            added.removeAll(p1c);
214    
215            // The minimal set of names that we must remove from the input properties set.
216            final Set<Object> deleted = new HashSet<Object>(p1.keySet());
217            deleted.removeAll(p2.keySet());
218    
219            // If there is probably not enough in common between the two instances
220            // for a diff to be likely to be efficient, then abort.
221            if(!force && (added.size() + deleted.size() > p2.size()/2))
222                { throw new DiffException("exhibit sets too different (force==false): "+(added.size() + deleted.size())); }
223    
224            // Create the "deleted" list as a Set of the names.
225            final Set<String> deletedNames = new HashSet<String>(2*deleted.size());
226            if(intern)
227                { for(final Object d : deleted) { deletedNames.add(MemoryTools.intern((String) d)); } }
228            else
229                { for(final Object d : deleted) { deletedNames.add((String) d); } }
230    
231            // We transform the "added"/changed names into a Map,
232            // including the diff from the previous version of the specific named set
233            // (if there was no previous version, we use an empty Properties set).
234            final Map<String,PropertiesDiff> addedMap = new HashMap<String, PropertiesDiff>(2*added.size());
235            for(final Tuple.Pair<String,Properties> entry : added)
236                {
237                final String name = entry.first;
238                final Properties oldPS = p1.get(name);
239                final PropertiesDiff pd = PropertiesDiff.createDiff(((null == oldPS) ? EMPTY_PROPERTIES : oldPS), entry.second, true, intern);
240                addedMap.put((intern ? MemoryTools.intern(name) : name), pd);
241                }
242            assert(added.size() == addedMap.size());
243    
244            // Create the diff object.
245            final PropertiesBundleDiff result = new PropertiesBundleDiff(
246                            p1.size(),
247                            p2.size(),
248                            deletedNames,
249                            addedMap);
250            return(result);
251            }
252    
253        /**Applies diff to an extant Properties instance to generate a new AEP instance; never null.
254         * The diff must only be applied to the same Properties instance value
255         * that the diff was generated from (ie an equal one).
256         * <p>
257         * We do very basic/quick sanity checks on the before/after sizes
258         * to catch misapplication of a diff to a clearly-inappropriate Properties set.
259         *
260         * @throws DiffException  if the diff cannot be applied
261         */
262        public static Map<String,Properties> applyDiff(final Map<String,Properties> bp1,
263                                                       final PropertiesBundleDiff diff)
264            throws DiffException
265            {
266            if((null == bp1) || (null == diff))
267                { throw new IllegalArgumentException(); }
268    
269            // Make sure that the diff "before" values match those from the input.
270            if(diff.sizeBefore != bp1.size())
271                { throw new DiffException("diff cannot be applied to these Properties"); }
272    
273            // Compute mutable Set of the Change values for the input AEP.
274            // We will mutate this to become the full exhibit data set for the result.
275            final Set<Tuple.Pair<String,Properties>> p1c = new HashSet<Tuple.Pair<String,Properties>>(_computeChangeValues(bp1));
276    
277            // Now synthesise a new Properties bundle.
278            final Map<String,Properties> result = new HashMap<String,Properties>(2 * diff.sizeAfter);
279    
280            // Copy the existing items into the result,
281            // except where they are marked as deleted.
282            // If there were some deleted items,
283            // then zap them here.
284            // They must ALL be present in the input.
285            final Set<String> del = diff.deletedNames;
286            final int initialSize = p1c.size();
287            for(final Iterator<Tuple.Pair<String,Properties>> cit = p1c.iterator(); cit.hasNext(); )
288                {
289                final Tuple.Pair<String,Properties> c = cit.next();
290                // Remove this name if noted as deleted.
291                if(del.contains(c.first))
292                    { cit.remove(); }
293                else
294                    { result.put(c.first, c.second); }
295                }
296            // All "deleted" exhibits must exist in the initial AEP.
297            if(initialSize - del.size() != p1c.size())
298                { throw new DiffException("invalid disjunction/gap between initial and deleted items: initialSize="+initialSize+", delSize="+(del.size())+", trimmedSize="+p1c.size()); }
299    
300            // If there were some added/changed items,
301            // then add them (or apply diffs) here.
302            for(final Map.Entry<String,PropertiesDiff> entry : diff.newValues.entrySet())
303                {
304                final String name = entry.getKey();
305                final Properties oldP = result.get(name);
306                final PropertiesDiff pd = entry.getValue();
307                final Properties newP = PropertiesDiff.applyDiff((oldP == null) ? EMPTY_PROPERTIES : oldP, pd);
308                result.put(name, newP);
309                }
310    
311            if(result.size() != diff.sizeAfter)
312                { throw new DiffException("unable to reconstruct aeid correctly"); }
313    
314            return(result);
315            }
316    
317    
318    
319        /**Returns true iff the diff is "empty", ie no deletions nor additions/changes. */
320        public boolean isEmpty()
321            { return(deletedNames.isEmpty() && newValues.isEmpty()); }
322    
323        /**Equal iff all fields are identical. */
324        @Override public boolean equals(final Object obj)
325            {
326            if(this == obj) { return(true); }
327            if(!(obj instanceof PropertiesBundleDiff)) { return(false); }
328            final PropertiesBundleDiff other = (PropertiesBundleDiff) obj;
329            if(sizeBefore != other.sizeBefore) { return(false); }
330            if(sizeAfter != other.sizeAfter) { return(false); }
331            return(deletedNames.equals(other.deletedNames) &&
332                            newValues.equals(other.newValues));
333            }
334    
335        /**Cached hash value; initially zero.
336         * We cache this to ensure that hashCode() always remains quick.
337         * <p>
338         * Since access is atomic, this need not be marked volatile.
339         * <p>
340         * Never set to zero once non-zero.
341         */
342        private transient int _hash;
343    
344        /**Hash code depends on all deleted/changed elements; guaranteed zero for an empty diff, non-zero otherwise. */
345        @Override public int hashCode()
346            {
347            // Try to use the cached value, if any.
348            int h = _hash;
349            if(h != 0) { return(h); }
350    
351            // Recompute as apparently not cached.
352            // Should very rarely be genuinely zero (causing wasteful recompution)
353            // unless the structures are empty and thus quick to rehash anyway.
354            //
355            // Note that hashCode for empty Set/Map is zero by definition,
356            // so if deletedNames and newValues are empty our result is zero.
357            // The "newValues" is often the more interesting component.
358            h = newValues.hashCode() + (67021*deletedNames.hashCode());
359            if((h == 0) && !isEmpty()) { h = 1; }
360            return(_hash = h);
361            }
362    
363        /**Human-readable summary of state. */
364        @Override public String toString()
365            {
366            return("PropertiesBundleDiff:deletedNames#"+deletedNames.size()+":newValues#"+newValues.size());
367            }
368    
369    
370        /**Write out a less-redundant form of our internal information.
371         * In particular, since deltas will not usually involve deletion of any values,
372         * we make the no-deletions case take no space at all in the serialised form.
373         */
374        private void writeObject(final ObjectOutputStream oos)
375            throws IOException
376            {
377            // Write the fields that we are not trying to optimise.
378            // Note that this includes our length field.
379            oos.defaultWriteObject();
380    
381            // Send the deleted names as bare Strings in sorted order
382            // to help with compression on the wire.
383            final int sizeD = deletedNames.size();
384            final String outD[] = new String[sizeD];
385            if(sizeD > 0) { deletedNames.toArray(outD); }
386            // We don't even write a length.
387            // The presence of the first non-String value is sufficient delimiter.
388            for(final String dn : outD)
389                { oos.writeObject(dn); }
390    
391            // Send the added/changed Properties sets in (name) sorted order
392            // to help with compression on the wire.
393            // We write the names and then the values.
394            final int sizeA = newValues.size();
395            final String outAK[] = new String[sizeA];
396            final PropertiesDiff outAV[] = new PropertiesDiff[sizeA];
397            if(sizeA > 0)
398                {
399                newValues.keySet().toArray(outAK);
400    //            newValues.values().toArray(outAV);
401                // Ensure correct correspondence between key index and value index.
402                for(int i = sizeA; --i >= 0; )
403                    { outAV[i] = newValues.get(outAK[i]); }
404                }
405            // We preserve the relative ordering so as not to scramble the mapping!
406            oos.writeObject(outAK);
407            oos.writeObject(outAV);
408            }
409    
410        /**Deserialise.
411         * This intern()s all the Properties set names.
412         */
413        private void readObject(final ObjectInputStream ois)
414            throws IOException, ClassNotFoundException
415            {
416            // Read the fields that we are not trying to optimise.
417            ois.defaultReadObject();
418    
419            // Get the deleted property names
420            // reading until we hit the change/additions array.
421            Object objectIn = null;
422            final HashSet<String> dn = new HashSet<String>();
423            while((objectIn = ois.readObject()) instanceof String)
424                {
425                if(!dn.add(MemoryTools.intern((String) objectIn)))
426                    { throw new InvalidObjectException("duplicate 'deleted' names"); }
427                }
428            deletedNames = Collections.unmodifiableSortedSet(
429                            new TreeSet<String>(dn));
430    
431            // Get the added/changed name-value details.
432            // Must be in the non-String item that we just read.
433            final String inAK[] = (String[]) objectIn;
434            final PropertiesDiff inAV[] = (PropertiesDiff[]) ois.readObject();
435            if(inAK.length != inAV.length) // Implicitly checks non-null too...
436                { throw new InvalidObjectException("damaged new/changed list"); }
437            final SortedMap<String,PropertiesDiff> nv = new TreeMap<String,PropertiesDiff>();
438            for(int i = inAK.length; --i >= 0; )
439                { nv.put(MemoryTools.intern(inAK[i]), inAV[i]); }
440            newValues = Collections.unmodifiableSortedMap(nv);
441    
442            // Validate the results.
443            validateObject();
444            }
445    
446        /**Deserialise: discard duplicate empty values. */
447        protected Object readResolve()
448            throws java.io.ObjectStreamException
449            {
450            if(equals(EMPTY_DIFF)) { return(EMPTY_DIFF); }
451            return(this);
452            }
453    
454        /**Checks that the object is internally consistent. */
455        public void validateObject() throws InvalidObjectException
456            {
457            if((sizeBefore < 0) || (sizeAfter < 0))
458                { throw new InvalidObjectException("bad object: -ve size"); }
459    
460            if((deletedNames == null) || (newValues == null))
461                { throw new InvalidObjectException("bad object: null deletedNames or newValues"); }
462    
463            final HashSet<String> overlap = new HashSet<String>(deletedNames);
464            overlap.retainAll(newValues.keySet());
465            if(!overlap.isEmpty())
466                { throw new InvalidObjectException("bad object: deletedNames and newValues overlap: "+overlap.size()); }
467    
468            try
469                {
470                if(deletedNames.contains(null))
471                    { throw new InvalidObjectException("bad object: deletedNames contains null value"); }
472                }
473            catch(final NullPointerException e) { /* Set does not support null values; good. */ }
474    
475            try
476                {
477                if(newValues.containsKey(null))
478                    { throw new InvalidObjectException("bad object: newValues contains null key"); }
479                }
480            catch(final NullPointerException e) { /* Map does not support null keys; good. */ }
481    
482            try
483                {
484                if(newValues.containsValue(null))
485                    { throw new InvalidObjectException("bad object: newValues contains null value"); }
486                }
487            catch(final NullPointerException e) { /* Map does not support null values; good. */ }
488            }
489    
490        /**Serial UID.*/
491        private static final long serialVersionUID = 4307342238100184047L;
492    
493    
494        /**Load a complete bundle of properties files with timestamp in one go; never null.
495         * The timestamp is that of the newest (most-recently-modified) bundle member.
496         * <p>
497         * We load all files of form <code>basename</code>*<code>.properties</code>
498         * in the given directory.
499         *
500         * @param dir  directory in which properties files are to be found; non-null
501         * @param basename  base name of properties files; non-null
502         *
503         * @throws FileNotFoundException  if directoiry not found or no bundle files found
504         * @throws IOException  in case of difficulty loading bundle files
505         */
506        public static Tuple.Pair< Map<String, Properties>, Long> loadBundle(final File dir,
507                                                                            final String basename)
508            throws IOException
509            {
510            if((dir == null) || (basename == null))
511                { throw new IllegalArgumentException(); }
512            if(!dir.exists() || !dir.isDirectory())
513                { throw new FileNotFoundException("cannot open bundle dir "+dir); }
514    
515            // Get all files in indicated directory.
516            final String fileSuffix = ".properties";
517            final String[] entries = dir.list(new FilenameFilter(){
518                public boolean accept(final File d, final String name)
519                    {
520                    return(name.startsWith(basename) && name.endsWith(fileSuffix) &&
521                                    (new File(d, name)).isFile());
522                    }
523                });
524    
525            long timestamp = -1; // No timestamp yet.
526            final Map<String, Properties> bundle = new HashMap<String, Properties>();
527            final int basenameLen = basename.length();
528            final int fileSuffixLen = fileSuffix.length();
529            for(final String f : entries)
530                {
531                final File path = new File(dir, f);
532                final Pair<Properties, Long> props = PropertiesDiff.loadFromFile(path);
533                final String shortName = f.substring(basenameLen, f.length() - fileSuffixLen);
534                bundle.put(shortName, props.first);
535                final long ts = props.second.longValue();
536                if(ts > timestamp) { timestamp = ts; }
537                }
538    
539            if((timestamp <= 0) || (bundle.isEmpty()))
540                { throw new FileNotFoundException("no suitable files in bundle dir "+dir); }
541            return(new Tuple.Pair< Map<String, Properties>, Long>(bundle, new Long(timestamp)));
542            }
543        }