001 /*
002 Copyright (c) 1996-2011, 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
030 package org.hd.d.pg2k.svrCore;
031
032 import java.io.IOException;
033 import java.io.InvalidObjectException;
034 import java.io.ObjectInputStream;
035 import java.io.ObjectOutputStream;
036 import java.io.Serializable;
037 import java.util.Arrays;
038 import java.util.HashMap;
039 import java.util.HashSet;
040 import java.util.Iterator;
041 import java.util.Map;
042 import java.util.Set;
043
044 /**Class to compute/represent changes from one AEP instance to another.
045 * Where a small number of changes has occurred between one AEP and another,
046 * especially where those changes are renamings or minor textual changes,
047 * it may often be vastly more efficient to send a diff from an extant
048 * AEP instance than send the new instance in toto.
049 * <p>
050 * There may be circumstances in which this class cannot compute a diff,
051 * or where the diff is unlikely to be worthwhile (eg after huge changes),
052 * in which case the diff computation routine indicates failure/unwillingness
053 * and the caller may have to arrange for a full AEP to be transmitted.
054 * <p>
055 * This class is Serializable, and is reasonably efficient on-the-wire,
056 * but is not necessarily intended for long-term persistence;
057 * store the AEP instances themselves for that by preference.
058 * <p>
059 * We are cautious in that we store several explicit before-and-after hashes
060 * to be clear that we are applying diffs to the right AEP instance
061 * and so as to be sure that we end up with the correct value.
062 *
063 * @author DHD
064 */
065 public final class AllExhibitPropertiesDelta implements Serializable
066 {
067 /**The long hash for the AEP to which the diff is to be applied; guaranteed non-negative. */
068 public final long longHashAEPBefore;
069
070 /**The long hash for the AEP result; guaranteed non-negative. */
071 public final long longHashAEPAfter;
072
073 /**The timestamp from the AEP result; guaranteed non-negative. */
074 public final long hashNotChangedSinceAfter;
075
076 /**The timestamp/hash for the AEIP in the AEP to which the diff is to be applied; guaranteed non-negative. */
077 public final long timestampAEIDBefore;
078
079 /**The timestamp/hash for the AEIP in the AEP result; guaranteed non-negative. */
080 public final long timestampAEIDAfter;
081
082 /**The exhibit count for the AEIP in the AEP to which the diff is to be applied; guaranteed non-negative. */
083 public final int lengthAEIDBefore;
084
085 /**The exhibit count for the AEIP in the AEP result; guaranteed non-negative. */
086 public final int lengthAEIDAfter;
087
088 /**The diff from the previous EPGI; may be null to represent no changes to EPGI value. */
089 private final ExhibitPropsGlobalImmutable.EPGIDiff epgiDiff;
090
091 /**The set of exhibits (full exhibit names) removed (or changed); may be null if no deletions/changes.
092 * Not serialised as-is.
093 * <p>
094 * Does not contain duplicates, nulls or invalid-syntax values.
095 * <p>
096 * This is all exhibits whose old definitions/data must be
097 * logically removed from the input AEP first.
098 */
099 private /* final */ transient Set<Name.ExhibitFull> exhibitsDeleted;
100
101 /**The set of exhibits (full exhibit names) added (or changed); may be null if no additions/changes.
102 * Not serialised as-is.
103 * <p>
104 * Does not contain duplicates, nulls or invalid-syntax values.
105 * <p>
106 * This is all exhibits whose old definitions/data must be
107 * logically added to the input AEP after the exhibitsDeleted set has been removed.
108 */
109 private /* final */ transient Set<Change> exhibitsAdded;
110
111
112 /**Serialisation ID. */
113 private static final long serialVersionUID = 8608225153676490831L;
114
115
116 /**Minimal checked exception thrown to indicate that diff could not be generated/applied. */
117 public static final class DiffException extends Exception
118 {
119 private static final long serialVersionUID = -330320938545639614L;
120 public DiffException() { super(); }
121 public DiffException(final String message) { super(message); }
122 public DiffException(final String message, final Throwable cause) { super(message, cause); }
123 }
124
125 /**Data for new/changed exhibit; immutable and serialisable. */
126 private static final class Change implements Serializable, Comparable<Change>
127 {
128 /**Serialisation ID. */
129 private static final long serialVersionUID = 1511290927439367951L;
130
131 public Change(final ExhibitStaticAttr esa,
132 final ExhibitPropsLoadable epl,
133 final ExhibitPropsComputable epc)
134 {
135 if(null == esa) { throw new IllegalArgumentException(); }
136 this.esa = esa;
137 this.epl = epl;
138 this.epc = epc;
139 }
140
141 /**ESA; never null. */
142 public final ExhibitStaticAttr esa;
143 /**EPL; may be null. */
144 public final ExhibitPropsLoadable epl;
145 /**EPC; may be null. */
146 public final ExhibitPropsComputable epc;
147
148 /**Equal iff all fields are identical. */
149 @Override public boolean equals(final Object obj)
150 {
151 if(this == obj) { return(true); }
152 if(!(obj instanceof Change)) { return(false); }
153 final Change other = (Change) obj;
154
155 if(null == epl) { if(null != other.epl) { return(false); } }
156 else if(!epl.equals(other.epl)) { return(false); }
157 if(null == epc) { if(null != other.epc) { return(false); } }
158 else if(!epc.equals(other.epc)) { return(false); }
159
160 final boolean esasIdentical = esa.isIdentical(other.esa);
161 //if(!esasIdentical && esa.equals(other.esa)) { System.err.println("Mismatch? "+esa+" vs "+other.esa); }
162 return(esasIdentical);
163 }
164 /**The hash is based on the the exhibit name. */
165 @Override public int hashCode()
166 { return(esa.getExhibitFullName().hashCode()); }
167 /**Provide a total ordering, using name as the primary sort field. */
168 public int compareTo(final Change other)
169 {
170 // Deal with esa fields first.
171 final int nameCompare = TextUtils.compare(esa.getCharSequence(), other.esa.getCharSequence());
172 if(nameCompare != 0) { return(nameCompare); }
173 if(esa.length < other.esa.length) { return(-1); }
174 if(esa.length > other.esa.length) { return(+1); }
175 if(esa.timestamp < other.esa.timestamp) { return(-1); }
176 if(esa.timestamp > other.esa.timestamp) { return(+1); }
177
178 // Deal with epl field.
179 // Fake a total ordering using the hash.
180 final int eplh = ((null == epl) ? 0 : epl.hashCode());
181 final int oeplh = ((null == other.epl) ? 0 : other.epl.hashCode());
182 if(eplh < oeplh) { return(-1); }
183 if(eplh > oeplh) { return(+1); }
184
185 // Deal with epc field.
186 // Fake a total ordering using the hash.
187 final int eplc = ((null == epc) ? 0 : epc.hashCode());
188 final int oeplc = ((null == other.epc) ? 0 : other.epc.hashCode());
189 if(eplc < oeplc) { return(-1); }
190 if(eplc > oeplc) { return(+1); }
191
192 // Items seem to be identical.
193 return(0);
194 }
195
196 /**Human-readable summary. */
197 @Override public String toString()
198 {
199 final StringBuilder sb = new StringBuilder();
200 sb.append("<Change:");
201 sb.append(esa);
202 if(null != epl) { sb.append(',').append(epl); }
203 if(null != epc) { sb.append(',').append(epc); }
204 sb.append(">");
205 return(sb.toString());
206 }
207 }
208
209
210 /**Construct an empty instance.
211 * This nominally represents the null diff/change
212 * from one empty AEP instance to another empty AEP instance.
213 */
214 public AllExhibitPropertiesDelta()
215 {
216 this(0, 0, 0, 0, 0, 0, 0, null, null, null);
217 }
218
219 /**Construct an instance.
220 * The Collection arguments are defensively copied.
221 * <p>
222 * The String and other complex values are assumed to already have been intern()ed
223 * as far as needed/possible, given that any diff instance may be short-lived.
224 */
225 public AllExhibitPropertiesDelta(final long longHashAEPBefore,
226 final long longHashAEPAfter,
227 final long timestampAEIDBefore,
228 final long timestampAEIDAfter,
229 final int lengthAEIDBefore,
230 final int lengthAEIDAfter,
231 final long hashNotChangedSinceAfter,
232 final ExhibitPropsGlobalImmutable.EPGIDiff epgiDiff,
233 final Set<Name.ExhibitFull> exhibitsDeleted,
234 final Set<Change> exhibitsAdded)
235 {
236 this.longHashAEPBefore = longHashAEPBefore;
237 this.longHashAEPAfter = longHashAEPAfter;
238 this.hashNotChangedSinceAfter = hashNotChangedSinceAfter;
239 this.timestampAEIDBefore = timestampAEIDBefore;
240 this.timestampAEIDAfter = timestampAEIDAfter;
241 this.lengthAEIDBefore = lengthAEIDBefore;
242 this.lengthAEIDAfter = lengthAEIDAfter;
243 this.epgiDiff = epgiDiff;
244
245 // Take defensive copies of mutable collection arguments.
246 this.exhibitsDeleted = ((exhibitsDeleted == null) || exhibitsDeleted.isEmpty()) ?
247 null : new HashSet<Name.ExhibitFull>(exhibitsDeleted);
248 this.exhibitsAdded = ((exhibitsAdded == null) || exhibitsAdded.isEmpty()) ?
249 null : new HashSet<Change>(exhibitsAdded);
250
251 // Verify object state.
252 try { validateObject(); }
253 catch(final InvalidObjectException e)
254 {
255 final IllegalArgumentException err = new IllegalArgumentException(e.getMessage());
256 err.initCause(e);
257 throw err;
258 }
259 }
260
261
262 /**Write out a less-redundant form of our internal information.
263 * In particular, since deltas will not usually involve deletion of any exhibits,
264 * we make the no-deletions case take no space at all in the serialised form.
265 */
266 private void writeObject(final ObjectOutputStream oos)
267 throws IOException
268 {
269 // Write the fields that we are not trying to optimise.
270 // Note that this includes our length field.
271 oos.defaultWriteObject();
272
273 // Send the deleted names as bare Strings in sorted order
274 // to help with compression on the wire.
275 final int sizeD = (null == exhibitsDeleted) ? 0 : exhibitsDeleted.size();
276 final Name.ExhibitFull outD[] = new Name.ExhibitFull[sizeD];
277 if(sizeD > 0) { exhibitsDeleted.toArray(outD); }
278 Arrays.sort(outD);
279 // We don't even write a length.
280 // The presence of the first non-String/non-Name value is sufficient delimiter.
281 for(final Name.ExhibitFull dn : outD)
282 { oos.writeObject(dn); }
283
284 // Send the added/changed exhibits in sorted order
285 // to help with compression on the wire.
286 final int sizeA = (null == exhibitsAdded) ? 0 : exhibitsAdded.size();
287 final Change outA[] = new Change[sizeA];
288 if(sizeA > 0) { exhibitsAdded.toArray(outA); }
289 Arrays.sort(outA);
290 oos.writeObject(outA);
291 }
292
293 /**Deserialise.
294 */
295 private void readObject(final ObjectInputStream ois)
296 throws IOException, ClassNotFoundException
297 {
298 // Read the fields that we are not trying to optimise.
299 ois.defaultReadObject();
300
301 // Get the deleted exhibit names
302 // reading until we hit the Change/additions array.
303 Object objectIn = null;
304 final HashSet<Name.ExhibitFull> deletedNames = new HashSet<Name.ExhibitFull>();
305 // Circumspect here to allow reading old-format (String) entries from the wire.
306 while((objectIn = ois.readObject()) instanceof CharSequence)
307 {
308 final Name.ExhibitFull fn = (objectIn instanceof Name.ExhibitFull) ?
309 (Name.ExhibitFull) objectIn : Name.ExhibitFull.create((String) objectIn);
310 if(!deletedNames.add(fn))
311 { throw new InvalidObjectException("duplicate 'deleted' exhibit names"); }
312 }
313 if(!deletedNames.isEmpty())
314 { exhibitsDeleted = new HashSet<Name.ExhibitFull>(deletedNames); }
315
316 // Get the added/changed exhibit details.
317 // Must be in the non-name item that we just read.
318 final Change inA[] = (Change[]) objectIn;
319 if(inA.length > 0)
320 {
321 exhibitsAdded = new HashSet<Change>(Arrays.asList(inA));
322 if(exhibitsAdded.size() != inA.length)
323 { throw new InvalidObjectException("duplicate 'added' exhibit details"); }
324 }
325
326 validateObject();
327 }
328
329 /**Validate fields/state.
330 * Called in the constructor and possibly after de-serialising.
331 * <p>
332 * Barf if something bad is found.
333 * (Maybe allow some extra info in debug version.)
334 */
335 public void validateObject()
336 throws InvalidObjectException
337 {
338 // Basic validation of hashes/timestamps.
339 if(longHashAEPBefore < 0)
340 { throw new InvalidObjectException("bad object: longHashAEPBefore < 0"); }
341 if(longHashAEPAfter < 0)
342 { throw new InvalidObjectException("bad object: longHashAEPAfter < 0"); }
343 if(timestampAEIDBefore < 0)
344 { throw new InvalidObjectException("bad object: timestampAEIDBefore < 0"); }
345 if(timestampAEIDAfter < 0)
346 { throw new InvalidObjectException("bad object: timestampAEIDAfter < 0"); }
347 if(lengthAEIDBefore < 0)
348 { throw new InvalidObjectException("bad object: lengthAEIDBefore < 0"); }
349 if(lengthAEIDAfter < 0)
350 { throw new InvalidObjectException("bad object: lengthAEIDAfter < 0"); }
351 if(hashNotChangedSinceAfter < 0)
352 { throw new InvalidObjectException("bad object: hashNotChangedSinceAfter < 0"); }
353
354
355 // Check that all "deleted" names are valid.
356 // (Other values will have already been validated.)
357 if(exhibitsDeleted != null)
358 {
359 for(final Name.ExhibitFull en : exhibitsDeleted)
360 {
361 if(null == en)
362 { throw new InvalidObjectException("bad object: null 'deleted' exhibit name"); }
363 // if(!ExhibitName.validNameSyntax(en))
364 // { throw new InvalidObjectException("bad object: illegal 'deleted' exhibit name"); }
365 }
366 }
367
368
369 // Check some basic relationships between fields.
370 // (We expect this to be done anyway in the AEP/AEID instances themselves.)
371 if((lengthAEIDBefore == 0) != (timestampAEIDBefore == 0))
372 { throw new InvalidObjectException("bad object: zero AEID (before) length and timestamp should go together"); }
373 if((lengthAEIDAfter == 0) != (timestampAEIDAfter == 0))
374 { throw new InvalidObjectException("bad object: zero AEID (after) length and timestamp should go together"); }
375 if((lengthAEIDBefore == 0) != (longHashAEPBefore == 0))
376 { throw new InvalidObjectException("bad object: zero AEID (before) length and AEP longHash should go together"); }
377 if((lengthAEIDAfter == 0) != (longHashAEPAfter == 0))
378 { throw new InvalidObjectException("bad object: zero AEID (after) length and AEP longHash should go together"); }
379 // TODO: add more such checks if it seems worthwhile to so do.
380 }
381
382 /**Create "change" values from AEP; never null.
383 * This is the set of values to be added to an empty AEP to regenerate this one
384 * (ignoring the EPGI).
385 */
386 private static Set<Change> _computeChangeValues(final AllExhibitProperties aep)
387 {
388 final Set<Change> result = new HashSet<Change>(2*aep.aeid.length);
389 for(final Name.ExhibitFull exhibitFullName : aep.aeid.getAllExhibitNamesSorted())
390 {
391 result.add(new Change(aep.aeid.getStaticAttr(exhibitFullName),
392 aep.getExhibitPropsLoadable(exhibitFullName),
393 aep.getExhibitPropsComputable(exhibitFullName)));
394 }
395 assert(result.size() == aep.aeid.length);
396 return(result);
397 }
398
399
400 /**Create diff between two AEP instances.
401 * This creates a diff to be applied to an instance of the first argument
402 * to recreate the second argument.
403 * <p>
404 * This will refuse to create a diff if it seems that
405 * the diff is unlikely to be useful, for example:
406 * <ul>
407 * <li>If one AEP has much more exhibits than the other
408 * (the diff may not be much smaller than sending the second AEP whole).
409 * </ul>
410 *
411 * @param force if true then force a diff to be produced
412 * even if this routine would normally refuse to do so on efficiency grounds
413 *
414 * @throws DiffException if no diff can be generated
415 * or is unlikely to be worthwhile to use
416 * (eg would be more than a fraction of the size of the second AEP)
417 */
418 public static AllExhibitPropertiesDelta createDiff(final AllExhibitProperties aep1,
419 final AllExhibitProperties aep2,
420 final boolean force)
421 throws DiffException
422 {
423 if((null == aep1) || (null == aep2)) { throw new IllegalArgumentException(); }
424
425 if(!force)
426 {
427 if((aep1.aeid.length > 2*aep2.aeid.length) ||
428 (aep2.aeid.length > 2*aep1.aeid.length))
429 { throw new DiffException("sizes too different (force==false): "+aep1.aeid.length+" vs "+aep2.aeid.length); }
430 }
431
432 final Set<Change> aep1c = _computeChangeValues(aep1);
433 final Set<Change> aep2c = _computeChangeValues(aep2);
434
435 // The items that we will need to have in our "added" list.
436 final Set<Change> added = new HashSet<Change>(aep2c);
437 added.removeAll(aep1c);
438
439 // The items that we will need to have in our deleted list
440 // (we'll only need names in the end for this).
441 final Set<Change> deleted = new HashSet<Change>(aep1c);
442 deleted.removeAll(aep2c);
443
444 // If there is probably not enough in common between the AEP instances
445 // for a diff to be likely to be efficient, then abort.
446 // Note that because these values are likely to pack/serialise much less efficiently
447 // than as part of a full AEP for a number of reasons
448 // the added/deleted items need to be a fairly small fraction of the target AEP size
449 // to be reasonably sure that the diff is worthwhile.
450 // In most normal cases, ie for small incremental updates, this will easily be the case.
451 if(!force && (added.size() + deleted.size() >= Math.min(aep1.aeid.length, aep2.aeid.length)/2))
452 { throw new DiffException("exhibit sets too different (force==false): added="+added.size()+" deleted="+deleted.size()); }
453
454 // If the EPGI has changed then we will need to include the diff.
455 final ExhibitPropsGlobalImmutable.EPGIDiff diffEPGI = aep1.epgi.equals(aep2.epgi) ? null :
456 ExhibitPropsGlobalImmutable.EPGIDiff.createDiff(aep1.epgi, aep2.epgi);
457
458 // Create the "deleted" list as a Set of the names.
459 // (Note that these names have already been intern()ed by ExhibitStaticAttr.)
460 final Set<Name.ExhibitFull> deletedNames = new HashSet<Name.ExhibitFull>(2*deleted.size());
461 for(final Change d : deleted)
462 { deletedNames.add(d.esa.getExhibitFullName()); }
463
464 // Create the diff object.
465 final AllExhibitPropertiesDelta result = new AllExhibitPropertiesDelta(
466 aep1.longHash,
467 aep2.longHash,
468 aep1.aeid.timestamp,
469 aep2.aeid.timestamp,
470 aep1.aeid.length,
471 aep2.aeid.length,
472 aep2.hashNotChangedSince,
473 diffEPGI,
474 deletedNames,
475 added);
476
477 // System.out.println(" *** AEP CREATE: DIFF="+result+"; AEP OUT="+aep2+"; AEP IN="+aep1);
478
479 assert(aep1.aeid.getAllExhibitNamesSorted().containsAll(deletedNames));
480 assert(aep2.equals(applyDiff(aep1, result)));
481
482 return(result);
483 }
484
485 /**Applies diff to an extant AEP instance to generate a new AEP instance; never null.
486 * The diff must only be applied to the same AEP instance value
487 * that the diff was generated from (ie an equal one).
488 *
489 * @throws DiffException if the diff cannot be applied
490 */
491 public static AllExhibitProperties applyDiff(final AllExhibitProperties aep1,
492 final AllExhibitPropertiesDelta diff)
493 throws DiffException
494 {
495 if((null == aep1) || (null == diff))
496 { throw new IllegalArgumentException(); }
497
498 // Make sure that the diff "before" values match those from the AEP.
499 if((diff.lengthAEIDBefore != aep1.aeid.length) ||
500 (diff.longHashAEPBefore != aep1.longHash) ||
501 (diff.timestampAEIDBefore != aep1.aeid.timestamp))
502 { throw new DiffException("diff cannot be applied to this AEP"); }
503
504 // Compute mutable Set of the Change values for the input AEP.
505 // We will mutate this to become the full exhibit data set for the result.
506 final Set<Change> aep1c = new HashSet<Change>(_computeChangeValues(aep1));
507
508 // If there were some deleted items,
509 // then zap them here.
510 final Set<Name.ExhibitFull> del = diff.exhibitsDeleted;
511 if(del != null)
512 {
513 final int initialSize = aep1c.size();
514 for(final Iterator<Change> cit = aep1c.iterator(); cit.hasNext(); )
515 {
516 final Change c = cit.next();
517 // Remove this exhibit if noted as deleted.
518 if(del.contains(c.esa.getExhibitFullName()))
519 { cit.remove(); }
520 }
521 // All "deleted" exhibits must exist in the initial AEP.
522 if(initialSize - del.size() != aep1c.size())
523 { throw new DiffException("invalid disjunction/gap between initial and deleted items: initialSize="+initialSize+", delSize="+(del.size())+", trimmedSize="+aep1c.size()); }
524 }
525
526 // If there were some added items,
527 // then add them here.
528 final Set<Change> add = diff.exhibitsAdded;
529 if(add != null)
530 {
531 // There should be no overlap between the remaining and added items.
532 final int remainingSize = aep1c.size();
533 aep1c.addAll(add);
534 if(remainingSize + add.size() != aep1c.size())
535 { throw new DiffException("invalid overlap between remaining and added items"); }
536 }
537
538 // Now synthesise a new AEID, etc, and build a new AEP.
539 final Set<ExhibitStaticAttr> esas = new HashSet<ExhibitStaticAttr>(2*aep1c.size());
540 final Map<Name.ExhibitFull, ExhibitPropsLoadable> loadedProps = new HashMap<Name.ExhibitFull, ExhibitPropsLoadable>(2*aep1c.size());
541 final Map<Name.ExhibitFull, ExhibitPropsComputable> computedProps = new HashMap<Name.ExhibitFull, ExhibitPropsComputable>(2*aep1c.size());
542 for(final Change c : aep1c)
543 {
544 final Name.ExhibitFull key = c.esa.getExhibitFullName();
545 esas.add(c.esa);
546 if((c.epl != null) && !c.epl.equals(ExhibitPropsLoadable.EMPTY))
547 { loadedProps.put(key, c.epl); }
548 if((c.epc != null) && !c.epc.equals(ExhibitPropsComputable.EMPTY))
549 { computedProps.put(key, c.epc); }
550 }
551 final AllExhibitImmutableData aeid = new AllExhibitImmutableData(esas, diff.timestampAEIDAfter);
552
553 // Either epgi is unchanged so store the original in the result,
554 // or else epgi has changed so apply the diff to get the new value.
555 final ExhibitPropsGlobalImmutable newEpgi = ((diff.epgiDiff == null) ? aep1.epgi :
556 (ExhibitPropsGlobalImmutable.EPGIDiff.applyDiff(aep1.epgi, diff.epgiDiff)));
557
558 final AllExhibitProperties result = new AllExhibitProperties(
559 null,
560 newEpgi,
561 aeid,
562 loadedProps,
563 computedProps,
564 diff.hashNotChangedSinceAfter);
565
566 if((result.aeid.length != diff.lengthAEIDAfter) ||
567 (result.aeid.timestamp != diff.timestampAEIDAfter))
568 { throw new DiffException("unable to reconstruct aeid correctly"); }
569 if(result.hashNotChangedSince != diff.hashNotChangedSinceAfter)
570 { throw new DiffException("unable to reconstruct aep.hashNotChangedSince correctly"); }
571 // We may not be able to reconstruct the AEP hash exactly (unless we force it)
572 // when restoring data from an old AEP version since, for example,
573 // the hash algorithm may have changed.
574 if(result.longHash != diff.longHashAEPAfter)
575 { throw new DiffException("unable to reconstruct aep.longHash correctly: diff="+diff+", putative aep="+result); }
576
577 return(result);
578 }
579
580 /**Human-readable summary. */
581 @Override
582 public final String toString()
583 {
584 final StringBuilder sb = new StringBuilder();
585 sb.append("AEPDelta");
586 if(exhibitsDeleted != null)
587 { sb.append(":deleted=").append(exhibitsDeleted.size()); /* .append(new ArrayList<String>(exhibitsDeleted)); */ }
588 if(exhibitsAdded != null)
589 { sb.append(":added=").append(exhibitsAdded.size()); /* .append(new ArrayList<Change>(exhibitsAdded)); */ }
590 return(sb.toString());
591 }
592 }