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 }