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        }