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.FileInputStream;
033    import java.io.FileNotFoundException;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.io.InvalidObjectException;
037    import java.io.ObjectInputStream;
038    import java.io.ObjectInputValidation;
039    import java.io.ObjectOutputStream;
040    import java.io.Serializable;
041    import java.util.Collections;
042    import java.util.Comparator;
043    import java.util.HashMap;
044    import java.util.HashSet;
045    import java.util.Iterator;
046    import java.util.Map;
047    import java.util.Map.Entry;
048    import java.util.concurrent.atomic.AtomicReference;
049    import java.util.NavigableMap;
050    import java.util.NavigableSet;
051    import java.util.Properties;
052    import java.util.Set;
053    import java.util.SortedMap;
054    import java.util.SortedSet;
055    import java.util.TreeMap;
056    import java.util.TreeSet;
057    
058    import org.hd.d.pg2k.svrCore.AllExhibitPropertiesDelta.DiffException;
059    import org.hd.d.pg2k.svrCore.FileTools;
060    import org.hd.d.pg2k.svrCore.MemoryTools;
061    import org.hd.d.pg2k.svrCore.Name;
062    import org.hd.d.pg2k.svrCore.TextUtils;
063    import org.hd.d.pg2k.svrCore.Tuple;
064    
065    /**An immutable diff/patch representation for well-behaved simple String-->String Properties.
066     * These diffs can be used to send just changes/deltas over the wire
067     * for well-behaved pure-String Properties.
068     * <p>
069     * Where all property names (ie keys) are pure 8-bit values
070     * a more compact internal (still immutable) form will be used for them.
071     * <p>
072     * Also individual values will be held in a more compact form where possible.
073     * <p>
074     * This class itself is immutable and Serializable,
075     * and designed to be efficient on the wire.
076     * <p>
077     * Note that comments are not retained,
078     * and the diff elements are ordered to allow efficient compression.
079     * <p>
080     * The diff notes elements that are removed
081     * and that are changed/added with the new/changed value in its entirety.
082     * <p>
083     * This does not record changes below the level of a single element,
084     * ie may not be effective for small number of large, often-changing values.
085     * <p>
086     * This class does not attempt to intern() keys or values unless asked to.
087     *
088     * @author DHD
089     */
090    public class PropertiesDiff implements Serializable, ObjectInputValidation
091        {
092        /**Comparator that provides total ordering for sorted results; not null. */
093        public static final Comparator<CharSequence> SORT_ORDER = TextUtils.CASE_SENSITIVE_ORDER;
094    
095        /**The logically immutable in-order set of items/names entirely removed; never null.
096         * This field is NOT serialised directly.
097         * <p>
098         * Keys in this set are always of a single type, eg all String or all Name.
099         */
100        private transient /*final*/ NavigableSet<CharSequence> deletedNames;
101    
102        /**The logically immutable in-order map from items/names to the new/changed values; never null.
103         * This field is NOT serialised directly.
104         * <p>
105         * Keys in this map are always of a single type, eg all String or all Name.
106         * <p>
107         * Values in this map may be of mixed type.
108         */
109        private transient /*final*/ NavigableMap<CharSequence, CharSequence> newValues;
110    
111        /**The entry count for the input properties set as a sanity-check; guaranteed non-negative. */
112        public final int sizeBefore;
113    
114        /**The entry count for the output properties set as a sanity-check; guaranteed non-negative. */
115        public final int sizeAfter;
116    
117        /**Immutable empty diff. */
118        public static final PropertiesDiff EMPTY_DIFF = new PropertiesDiff();
119    
120        /**Make new empty diff instance. */
121        public PropertiesDiff()
122            { this(0, 0, Collections.<CharSequence>emptySet(), Collections.<CharSequence,CharSequence>emptyMap()); }
123    
124        /**Create diff instance.
125         * We defensively copy the potentially-mutable Collection argument,
126         * and perform optimisation on internal representation.
127         *
128         * @param sizeBefore  size of input Properties; non-negative
129         * @param sizeAfter   size of output Properties; non-negative
130         * @param deleted     names present in input and not present in output; non-null
131         * @param added       new/changed name-value pairs in output; non-null
132         */
133        public PropertiesDiff(final int sizeBefore, final int sizeAfter,
134                              final Set<? extends CharSequence> deleted,
135                              final Map<? extends CharSequence, ? extends CharSequence> added)
136            {
137            // Convert all deleted names to (same) maximally-compact form.
138            deletedNames = convertAllToNameOrAllToString(deleted);
139            // Convert keys to (same) maximally-compact form.
140            // Convert values individually to most-compact form available.
141            // Get compacted form of keys, of equivalent values to original set.
142            final NavigableSet<CharSequence> aConvKeys = convertAllToNameOrAllToString(added.keySet());
143            assert(aConvKeys.equals(added.keySet()));
144            // Create sorted set of optimised values, eliminating duplicates, insensitive to value type.
145            final NavigableSet<CharSequence> uniqueValues = new TreeSet<CharSequence>(SORT_ORDER);
146            final AtomicReference<Name> prevRef = new AtomicReference<Name>();
147            for(final CharSequence v : added.values())
148                {
149                assert(null != v);
150                // If we already have a version of this value then skip.
151                if(uniqueValues.contains(v)) { continue; }
152                // Convert and store.
153                uniqueValues.add(TextUtils.makeEfficientTextRepresentation(v, prevRef));
154                }
155            assert(uniqueValues.size() <= added.size());
156            // Create SortedMap from input which I can look up in with possibly-converted keys.
157            final NavigableMap<CharSequence,CharSequence> a = new TreeMap<CharSequence,CharSequence>(SORT_ORDER);
158            a.putAll(added); // Copy in initial set.
159            // Replace all entries with de-duped / optimised values.
160            for(final CharSequence aConvKey : aConvKeys)
161                {
162                // Convert value to Name if possible, else use (less-compact) String.
163                final CharSequence value = a.get(aConvKey);
164                assert(null != value);
165                final CharSequence vcs = uniqueValues.floor(value);
166                assert(TextUtils.contentEqualsOrBothNull(vcs, value));
167                a.put(aConvKey, vcs); // Replace with optimised values.
168                }
169            assert(a.size() == added.size());
170            newValues = a;
171            this.sizeBefore = sizeBefore;
172            this.sizeAfter = sizeAfter;
173            try { validateObject(); }
174            catch(final InvalidObjectException e) { throw new IllegalArgumentException(e); }
175            }
176    
177        /**Convert all supplied CharSequence values to Name else convert them all to String; never null.
178         * Values already of the correct type are not converted,
179         * and all String values will be intern()ed.
180         */
181        private static final NavigableSet<CharSequence> convertAllToNameOrAllToString(final Set<? extends CharSequence> in)
182            {
183            // Get the values into sorted order
184            // since that may make for a more compact representation.
185            final NavigableSet<CharSequence> ordered;
186            if(in instanceof NavigableSet) { ordered = ((NavigableSet<CharSequence>) in); }
187            else
188                {
189                ordered = new TreeSet<CharSequence>(SORT_ORDER); // Use our standard Comparator.
190                ordered.addAll(in);
191                }
192    
193            // Convert the values as necessary.
194            final NavigableSet<CharSequence> result = new TreeSet<CharSequence>(SORT_ORDER); // Use our standard Comparator.
195            try
196                {
197                Name prev = null;
198                for(final CharSequence cs : ordered)
199                    {
200                    // Leave existing Name values as is (not derived values).
201                    if(cs.getClass() == Name.class)
202                        { result.add(cs); }
203                    else
204                        { result.add(prev = Name.create(cs, prev)); }
205                    }
206    //System.out.println("Converted to Name Set of length " + in.size());
207                }
208            catch(final IllegalArgumentException e)
209                {
210                // Conversion failed, so start again, and convert all to String.
211                result.clear();
212                for(final CharSequence cs : in)
213                    {
214                    // Leave existing String values as is (not derived values).
215                    if(cs.getClass() == String.class)
216                        { result.add(cs); }
217                    else
218                        { result.add(MemoryTools.intern((new StringBuilder(cs)).toString())); }
219                    }
220    //System.out.println("Forced conversion to String Set of length " + in.size());
221                }
222            assert(result.size() == in.size());
223            return(result);
224            }
225    
226        /**Get (immutable) newValues; never null.
227         * Keys are always of a single (immutable) type such as String for each instance,
228         * that should work in hash or sorted collections.
229         * <p>
230         * Values may be of a mix of (immutable) types within one result.
231         * <p>
232         * The comparator is SORT_ORDER
233         * and we use a SortedMap so as not to rely on hashCode() and equals(),
234         * eg we can look up with a String key in a table whose actual keys are Name.
235         */
236        public SortedMap<CharSequence, CharSequence> getNewValues()
237            { return(Collections.unmodifiableSortedMap(newValues)); }
238    
239        /**Create "change" values from a Properties instance; never null.
240         * This is the set of values to be added to an empty Properties value
241         * to regenerate this one.
242         * <p>
243         * The first element in each Pair is the (non-null) property name;
244         * the second element is the (non-null) property value.
245         *
246         * @param p non-null pure String-->String properties set
247         */
248        private static Set<Tuple.Pair<CharSequence,CharSequence>> _computeChangeValues(final Properties p)
249            {
250            final Set<Tuple.Pair<CharSequence,CharSequence>> result = new HashSet<Tuple.Pair<CharSequence,CharSequence>>(2*p.size());
251            for(final Map.Entry entry : p.entrySet())
252                {
253                result.add(new Tuple.Pair<CharSequence,CharSequence>((CharSequence) entry.getKey(),
254                                                                     (CharSequence) entry.getValue()));
255                }
256            assert(result.size() == p.size());
257            return(result);
258            }
259    
260        /**Private empty Properties map; never changed, never null. */
261        private static final Properties EMPTY_PROPERTIES = new Properties();
262    
263        /**Create diff against empty Properties.
264         * This is a safe, efficient, immutable, Serializable representation
265         * for a single Properties instance,
266         * where a raw Properties instance may not be safe/robust enough.
267         * <p>
268         * Forces intern()ing of the keys and values.
269         */
270        public static PropertiesDiff createAsStandAloneDiff(final Properties p1)
271            {
272            try { return(createDiff(EMPTY_PROPERTIES, p1, true, true)); }
273            catch(final DiffException e) { throw new Error(e); } // Should never happen.
274            }
275    
276        /**Create from diff against empty Properties.
277         * Recovers Properties instance created by createAsDiff.
278         */
279        public static Properties createFromStandAloneDiff(final PropertiesDiff pd)
280            throws DiffException
281            { return(applyDiff(EMPTY_PROPERTIES, pd)); }
282    
283        /**Create diff between two Properties instances; never null.
284         * This creates a diff to be applied to an instance of the first argument
285         * to recreate the second argument.
286         * <p>
287         * This will refuse to create a diff if it seems that
288         * the diff is unlikely to be useful, for example:
289         * <ul>
290         * <li>If one properties set has more than twice the entries of the other
291         *     (the diff may not be smaller than sending the second properties set whole).
292         * </ul>
293         *
294         * @param force  if true then force a diff to be produced
295         *     even if this routine would normally refuse to do so on efficiency grounds
296         * @param intern  if true than force intern()ing of all String values
297         *
298         * @throws DiffException  if no diff can be generated
299         *     or is unlikely to be worthwhile to use
300         *     (eg would be more than a fraction of the size of the second AEP)
301         */
302        public static PropertiesDiff createDiff(final Properties p1,
303                                                final Properties p2,
304                                                final boolean force,
305                                                final boolean intern)
306            throws DiffException
307            {
308            if((null == p1) || (null == p2))
309                { throw new IllegalArgumentException(); }
310    
311            if(!force)
312                {
313                if((p1.size() > 2*p2.size()) ||
314                   (p2.size() > 2*p1.size()))
315                    { throw new DiffException("sizes too different (force==false)"); }
316                }
317    
318            final Set<Tuple.Pair<CharSequence,CharSequence>> p1c = _computeChangeValues(p1);
319            final Set<Tuple.Pair<CharSequence,CharSequence>> p2c = _computeChangeValues(p2);
320    
321            // The items that we will need to have in our "added" list.
322            final Set<Tuple.Pair<CharSequence,CharSequence>> added = new HashSet<Tuple.Pair<CharSequence,CharSequence>>(p2c);
323            added.removeAll(p1c);
324    
325            // The minimal set of names that we must remove from the input properties set.
326            final Set<Object> deleted = new HashSet<Object>(p1.keySet());
327            deleted.removeAll(p2.keySet());
328    
329            // If there is probably not enough in common between the two instances
330            // for a diff to be likely to be efficient, then abort.
331            if(!force && (added.size() + deleted.size() > p2.size()/2))
332                { throw new DiffException("exhibit sets too different (force==false): "+(added.size() + deleted.size())); }
333    
334            // Create the "deleted" list as a Set of the names.
335            final Set<String> deletedNames = new HashSet<String>(2*deleted.size());
336            if(intern)
337                { for(final Object d : deleted) { deletedNames.add(MemoryTools.intern((String) d)); } }
338            else
339                { for(final Object d : deleted) { deletedNames.add((String) d); } }
340    
341            // We transform the "added"/changed names into a Map.
342            final Map<CharSequence,CharSequence> addedMap = new HashMap<CharSequence, CharSequence>(2*added.size());
343            if(intern)
344                {
345                for(final Tuple.Pair<CharSequence,CharSequence> entry : added)
346                    { addedMap.put(MemoryTools.intern(entry.first), MemoryTools.intern(entry.second)); }
347                }
348            else
349                {
350                for(final Tuple.Pair<CharSequence,CharSequence> entry : added)
351                    { addedMap.put(entry.first, entry.second); }
352                }
353            assert(added.size() == addedMap.size());
354    
355            // Create the diff object.
356            final PropertiesDiff result = new PropertiesDiff(
357                            p1.size(),
358                            p2.size(),
359                            deletedNames,
360                            addedMap);
361            return(result);
362            }
363    
364        /**Applies diff to an extant Properties instance to generate a new AEP instance; never null.
365         * The diff must only be applied to the same Properties instance value
366         * that the diff was generated from (ie an equal one).
367         * <p>
368         * We do very basic/quick sanity checks on the before/after sizes
369         * to catch misapplication of a diff to a clearly-inappropriate Properties set.
370         *
371         * @throws DiffException  if the diff cannot be applied
372         */
373        public static Properties applyDiff(final Properties p1,
374                                           final PropertiesDiff diff)
375            throws DiffException
376            {
377            if((null == p1) || (null == diff))
378                { throw new IllegalArgumentException(); }
379    
380            // Make sure that the diff "before" values match those from the input.
381            if(diff.sizeBefore != p1.size())
382                { throw new DiffException("diff cannot be applied to these Properties"); }
383    
384            // Compute mutable Set of the Change values for the input AEP.
385            // We will mutate this to become the full exhibit data set for the result.
386            final Set<Tuple.Pair<CharSequence,CharSequence>> p1c = new HashSet<Tuple.Pair<CharSequence,CharSequence>>(_computeChangeValues(p1));
387    
388            // Now synthesise a new Properties map.
389            final Properties result = new Properties();
390    
391            // Copy the existing items into the result,
392            // except where they are marked as deleted.
393            // If there were some deleted items,
394            // then zap them here.
395            // They must ALL be present in the input.
396            final Set<CharSequence> del = diff.deletedNames;
397            final int initialSize = p1c.size();
398            for(final Iterator<Tuple.Pair<CharSequence,CharSequence>> cit = p1c.iterator(); cit.hasNext(); )
399                {
400                final Tuple.Pair<CharSequence,CharSequence> c = cit.next();
401                // Remove this name if noted as deleted.
402                if(del.contains(c.first))
403                    { cit.remove(); }
404                else
405                    { result.setProperty(c.first.toString(), c.second.toString()); }
406                }
407            // All "deleted" exhibits must exist in the initial AEP.
408            if(initialSize - del.size() != p1c.size())
409                { throw new DiffException("invalid disjunction/gap between initial and deleted items: initialSize="+initialSize+", delSize="+(del.size())+", trimmedSize="+p1c.size()); }
410    
411            // If there were some added/changed items,
412            // then add/override them here.
413            for(final Map.Entry<CharSequence,CharSequence> entry : diff.newValues.entrySet())
414                { result.setProperty(entry.getKey().toString(), entry.getValue().toString()); }
415    
416            if(result.size() != diff.sizeAfter)
417                { throw new DiffException("unable to reconstruct aeid correctly"); }
418    //        if(result.hashNotChangedSince != diff.hashNotChangedSinceAfter)
419    //            { throw new DiffException("unable to reconstruct aep.hashNotChangedSince correctly"); }
420    //        if(result.longHash != diff.longHashAEPAfter)
421    //            { throw new DiffException("unable to reconstruct aep.longHash correctly"); }
422    
423            return(result);
424            }
425    
426        /**Returns true iff the diff is "empty", ie no deletions nor additions/changes. */
427        public boolean isEmpty()
428            { return(deletedNames.isEmpty() && newValues.isEmpty()); }
429    
430        /**Equal iff all fields are identical. */
431        @Override public boolean equals(final Object obj)
432            {
433            if(this == obj) { return(true); }
434            if(!(obj instanceof PropertiesDiff)) { return(false); }
435            final PropertiesDiff other = (PropertiesDiff) obj;
436            if(sizeBefore != other.sizeBefore) { return(false); }
437            if(sizeAfter != other.sizeAfter) { return(false); }
438            return(deletedNames.equals(other.deletedNames) &&
439                   newValues.equals(other.newValues));
440            }
441    
442        /**Cached hash value; initially zero.
443         * We cache this because (re)computing it requires
444         * looking at every char in every key and value,
445         * ie potentially horribly expensive.
446         * <p>
447         * Since access is atomic, this need not be marked volatile.
448         * <p>
449         * Never set to zero once non-zero.
450         */
451        private transient int _hash;
452    
453        /**Hash code depends on all deleted/changed elements; guaranteed zero for an empty diff, non-zero otherwise. */
454        @Override public int hashCode()
455            {
456            // Try to use the cached value, if any.
457            int h = _hash;
458            if(h != 0) { return(h); }
459    
460            // Recompute as apparently not cached.
461            // Should very rarely be genuinely zero (causing wasteful recomputation)
462            // unless the structures are empty and thus quick to rehash anyway.
463            //
464            // Note that hashCode for empty Set/Map is zero by definition,
465            // so if deletedNames and newValues are empty our result is zero.
466            // The "newValues" is often the more interesting component.
467            h = (deletedNames.hashCode()*257) ^ newValues.hashCode();
468            if((h == 0) && !isEmpty()) { h = 1; }
469            _hash = h; // Cache it...
470            return(h);
471            }
472    
473        /**Human-readable summary of state. */
474        @Override public String toString()
475            {
476            final StringBuilder sb = new StringBuilder("PropertiesDiff");
477            final int sizeD = deletedNames.size();
478            if(sizeD > 0) { sb.append(":deletedNames#").append(sizeD); }
479            final int sizeN = newValues.size();
480            if(sizeN > 0) { sb.append(":newValues#").append(sizeN); }
481            return(sb.toString());
482            }
483    
484        /**Write out a less-redundant form of our internal information.
485         * In particular, since deltas will not usually involve deletion of any values,
486         * we make the no-deletions case take no space at all in the serialised form.
487         * <p>
488         * None of the values written are intern()ed during serialisation,
489         * at the risk of not unifying them with identical values in the object stream,
490         * to avoid inadvertently creating permanent intern() overhead for them.
491         */
492        private void writeObject(final ObjectOutputStream oos)
493            throws IOException
494            {
495            // Write the fields that we are not trying to optimise.
496            // Note that this includes our length field.
497            oos.defaultWriteObject();
498    
499            // Send the deleted names as bare Strings in sorted order
500            // to help with compression on the wire.
501            final int sizeD = deletedNames.size();
502            final CharSequence outD[] = new CharSequence[sizeD];
503            if(sizeD > 0) { deletedNames.toArray(outD); }
504            // Write all the deleted names.
505            // The presence of the first non-CharSequence value after is sufficient delimiter.
506            for(final CharSequence dn : outD)
507                { oos.writeObject(dn); }
508    
509            // Collate all the n keys followed by n corresponding values
510            // into a single CharSequence array to write.
511            // They are in sorted order by key to maximise stream compression opportunities.
512            final int n = newValues.size();
513            final CharSequence nv[] = new CharSequence[2*n];
514            int i = 0;
515            for(final Entry<CharSequence, CharSequence> e : newValues.entrySet())
516                {
517                nv[i] = e.getKey();
518                nv[i + n] = e.getValue();
519                ++i;
520                }
521            oos.writeObject(nv);
522    
523    //        // Send the added/changed name-value pairs in (key) sorted order
524    //        // to help with compression on the wire.
525    //        // We write the keys and then the values.
526    //        final int sizeA = newValues.size();
527    //        final String outAK[] = (sizeA == 0) ? NO_STRINGS : new String[sizeA];
528    //        final String outAV[] = (sizeA == 0) ? NO_STRINGS : new String[sizeA];
529    //        if(sizeA > 0)
530    //            {
531    //            final CharSequence csk[] = newValues.keySet().toArray(new CharSequence[sizeA]);
532    ////            newValues.values().toArray(outAV);
533    //            // Ensure correct correspondence between key index and value index.
534    //            for(int i = sizeA; --i >= 0; )
535    //                {
536    //                outAK[i] = csk[i].toString();
537    //                outAV[i] = newValues.get(csk[i]).toString();
538    //                }
539    //            }
540    //        // We preserve the relative ordering so as not to scramble the mapping!
541    //        oos.writeObject(outAK);
542    //        oos.writeObject(outAV);
543            }
544    
545        /**Deserialise.
546         * This doesn't work very hard to intern() or optimise or validate state,
547         * since readResolve() will pass all the data to a constructor to deal with.
548         */
549        private void readObject(final ObjectInputStream ois)
550            throws IOException, ClassNotFoundException
551            {
552            // Read the fields that we are not trying to optimise.
553            ois.defaultReadObject();
554    
555            // Get the deleted property names
556            // reading until we hit the change/additions array.
557            Object objectIn = null;
558            final HashSet<CharSequence> dn = new HashSet<CharSequence>();
559            while((objectIn = ois.readObject()) instanceof CharSequence)
560                {
561                if(!dn.add(MemoryTools.intern((CharSequence) objectIn)))
562                    { throw new InvalidObjectException("duplicate 'deleted' names"); }
563                }
564            final TreeSet<CharSequence> dts = new TreeSet<CharSequence>(SORT_ORDER);
565            dts.addAll(dn);
566            deletedNames = dts;
567    
568            // Get the added/changed name-value details.  // FIXME still must be pure String values...
569            // Must be in the non-String item that we just read.
570    
571            // If type of item just read is String[]
572            // then this is old-style String[] keys, String[] values.
573            final boolean oldStyleStringString = objectIn instanceof String[];
574            final NavigableMap<CharSequence,CharSequence> nv = new TreeMap<CharSequence,CharSequence>(SORT_ORDER);
575            if(oldStyleStringString)
576                {
577                final String inAK[] = (String[]) objectIn;
578                final String inAV[] = (String[]) ois.readObject();
579                if(inAK.length != inAV.length) // Implicitly checks non-null too...
580                    { throw new InvalidObjectException("damaged new/changed list"); }
581                for(int i = inAK.length; --i >= 0; )
582                    { nv.put(inAK[i], inAV[i]); }
583                }
584            else
585                {
586                // New style...
587                // Length 2n array of n CharSequence keys then n CharSequence values.
588                final CharSequence keysAndValues[] = (CharSequence[]) objectIn;
589                if((keysAndValues.length & 1) != 0)
590                    { throw new InvalidObjectException("invalid odd-length key/value array"); }
591                final int n = keysAndValues.length / 2;
592                for(int i = 0; i < n; ++i)
593                    { nv.put(keysAndValues[i], keysAndValues[i + n]); }
594                }
595            newValues = nv;
596            }
597    
598        /**Deserialise: including discarding duplicate empty values. */
599        protected Object readResolve()
600            throws java.io.ObjectStreamException
601            {
602            if(equals(EMPTY_DIFF)) { return(EMPTY_DIFF); }
603            // Defensive copy, optimisation, validation.
604            return(new PropertiesDiff(sizeBefore, sizeAfter, deletedNames, newValues));
605            }
606    
607        /**Checks that the object is internally consistent. */
608        public void validateObject() throws InvalidObjectException
609            {
610            if((sizeBefore < 0) || (sizeAfter < 0))
611                { throw new InvalidObjectException("bad object: -ve size"); }
612    
613            if((deletedNames == null) || (newValues == null))
614                { throw new InvalidObjectException("bad object: null deletedNames or newValues"); }
615    
616            final HashSet<CharSequence> overlap = new HashSet<CharSequence>(deletedNames);
617            overlap.retainAll(newValues.keySet());
618            if(!overlap.isEmpty())
619                { throw new InvalidObjectException("bad object: deletedNames and newValues overlap: "+overlap.size()); }
620    
621            try
622                {
623                if(deletedNames.contains(null))
624                    { throw new InvalidObjectException("bad object: deletedNames contains null value"); }
625                }
626            catch(final NullPointerException e) { /* Set does not support null values; good. */ }
627    
628            try
629                {
630                if(newValues.containsKey(null))
631                    { throw new InvalidObjectException("bad object: newValues contains null key"); }
632                }
633            catch(final NullPointerException e) { /* Map does not support null keys; good. */ }
634    
635            try
636                {
637                if(newValues.containsValue(null))
638                    { throw new InvalidObjectException("bad object: newValues contains null value"); }
639                }
640            catch(final NullPointerException e) { /* Map does not support null values; good. */ }
641            }
642    
643        /**Serial UID.*/
644        private static final long serialVersionUID = 4307342238100184046L;
645    
646    
647        /**Load properties with timestamp; never null.
648         * Requires "end=OK" (trailing) token to indicate undamaged file.
649         * @throws IOException  in case of difficulty loading the file
650         * @throws FileNotFoundException  if the file is absent/unreadable
651         */
652        public static Tuple.Pair<Properties,Long> loadFromFile(final File filename)
653            throws IOException
654            {
655            if(filename == null) { throw new IllegalArgumentException(); }
656    
657            if(!filename.canRead() || !filename.isFile())
658                { throw new FileNotFoundException(filename.getPath()); }
659    
660            final long timestamp = filename.lastModified();
661            final InputStream is = new FileInputStream(filename);
662            try
663                {
664                final Properties p = FileTools.loadProperties(is);
665                return(new Tuple.Pair<Properties,Long>(p, new Long(timestamp)));
666                }
667            finally { is.close(); }
668            }
669        }