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 }