001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.connector.federation.merge;
023    
024    import java.io.InvalidClassException;
025    import java.io.Serializable;
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.HashMap;
029    import java.util.HashSet;
030    import java.util.Iterator;
031    import java.util.Map;
032    import java.util.Set;
033    import java.util.concurrent.locks.ReadWriteLock;
034    import java.util.concurrent.locks.ReentrantReadWriteLock;
035    import net.jcip.annotations.GuardedBy;
036    import net.jcip.annotations.ThreadSafe;
037    import org.jboss.dna.common.CommonI18n;
038    import org.jboss.dna.common.util.CheckArg;
039    import org.jboss.dna.connector.federation.contribution.Contribution;
040    import org.jboss.dna.connector.federation.contribution.EmptyContribution;
041    import org.jboss.dna.graph.properties.DateTime;
042    import org.jboss.dna.graph.properties.Name;
043    import org.jboss.dna.graph.properties.Property;
044    
045    /**
046     * This class represents the details about how information from different sources are merged into a single federated node.
047     * <p>
048     * A merge plan basically consists of the individual contribution from each source and the information about how these
049     * contributions were merged into the single federated node.
050     * </p>
051     * <p>
052     * Merge plans are designed to be {@link Serializable serializable}, as they are persisted on the federated node and deserialized
053     * to assist in the management of the federated node.
054     * </p>
055     * 
056     * @author Randall Hauch
057     */
058    @ThreadSafe
059    public abstract class MergePlan implements Serializable, Iterable<Contribution> {
060    
061        public static MergePlan create( Contribution... contributions ) {
062            CheckArg.isNotNull(contributions, "contributions");
063            switch (contributions.length) {
064                case 0:
065                    throw new IllegalArgumentException(CommonI18n.argumentMayNotBeEmpty.text("contributions"));
066                case 1:
067                    return new OneContributionMergePlan(contributions[0]);
068                case 2:
069                    return new TwoContributionMergePlan(contributions[0], contributions[1]);
070                case 3:
071                    return new ThreeContributionMergePlan(contributions[0], contributions[1], contributions[2]);
072                case 4:
073                    return new FourContributionMergePlan(contributions[0], contributions[1], contributions[2], contributions[3]);
074                case 5:
075                    return new FiveContributionMergePlan(contributions[0], contributions[1], contributions[2], contributions[3],
076                                                         contributions[4]);
077                default:
078                    return new MultipleContributionMergePlan(contributions);
079            }
080        }
081    
082        public static MergePlan create( Collection<Contribution> contributions ) {
083            CheckArg.isNotNull(contributions, "contributions");
084            Iterator<Contribution> iter = contributions.iterator();
085            switch (contributions.size()) {
086                case 0:
087                    throw new IllegalArgumentException(CommonI18n.argumentMayNotBeEmpty.text("contributions"));
088                case 1:
089                    return new OneContributionMergePlan(iter.next());
090                case 2:
091                    return new TwoContributionMergePlan(iter.next(), iter.next());
092                case 3:
093                    return new ThreeContributionMergePlan(iter.next(), iter.next(), iter.next());
094                case 4:
095                    return new FourContributionMergePlan(iter.next(), iter.next(), iter.next(), iter.next());
096                case 5:
097                    return new FiveContributionMergePlan(iter.next(), iter.next(), iter.next(), iter.next(), iter.next());
098                default:
099                    return new MultipleContributionMergePlan(contributions);
100            }
101        }
102    
103        public static MergePlan addContribution( MergePlan plan,
104                                                 Contribution contribution ) {
105            CheckArg.isNotNull(plan, "plan");
106            CheckArg.isNotNull(contribution, "contribution");
107            if (plan instanceof MultipleContributionMergePlan) {
108                ((MultipleContributionMergePlan)plan).addContribution(contribution);
109                return plan;
110            }
111            MergePlan newPlan = null;
112            if (plan instanceof OneContributionMergePlan) {
113                newPlan = new TwoContributionMergePlan(plan.iterator().next(), contribution);
114            } else if (plan instanceof TwoContributionMergePlan) {
115                Iterator<Contribution> iter = plan.iterator();
116                newPlan = new ThreeContributionMergePlan(iter.next(), iter.next(), contribution);
117            } else if (plan instanceof ThreeContributionMergePlan) {
118                Iterator<Contribution> iter = plan.iterator();
119                newPlan = new FourContributionMergePlan(iter.next(), iter.next(), iter.next(), contribution);
120            } else if (plan instanceof FourContributionMergePlan) {
121                Iterator<Contribution> iter = plan.iterator();
122                newPlan = new FiveContributionMergePlan(iter.next(), iter.next(), iter.next(), iter.next(), contribution);
123            } else {
124                MultipleContributionMergePlan multiPlan = new MultipleContributionMergePlan();
125                for (Contribution existingContribution : plan) {
126                    multiPlan.addContribution(existingContribution);
127                }
128                multiPlan.addContribution(contribution);
129                newPlan = multiPlan;
130            }
131            newPlan.setAnnotations(plan.getAnnotations());
132            return newPlan;
133        }
134    
135        /**
136         * Define the earliest version of this class that is supported. The Java runtime, upon deserialization, compares the
137         * serialized object's version to this, and if less than this version will throw a {@link InvalidClassException}. If, however,
138         * the serialized object's version is compatible with this class, it will be deserialized successfully.
139         * <p>
140         * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/version.html#6678">Sun's documentation</a> describes
141         * the following changes can be made without negatively affecting the deserialization of older versions:
142         * <ul>
143         * <li>Adding fields - When the class being reconstituted has a field that does not occur in the stream, that field in the
144         * object will be initialized to the default value for its type. If class-specific initialization is needed, the class may
145         * provide a readObject method that can initialize the field to nondefault values.</i>
146         * <li>Adding classes - The stream will contain the type hierarchy of each object in the stream. Comparing this hierarchy in
147         * the stream with the current class can detect additional classes. Since there is no information in the stream from which to
148         * initialize the object, the class's fields will be initialized to the default values.</i>
149         * <li>Removing classes - Comparing the class hierarchy in the stream with that of the current class can detect that a class
150         * has been deleted. In this case, the fields and objects corresponding to that class are read from the stream. Primitive
151         * fields are discarded, but the objects referenced by the deleted class are created, since they may be referred to later in
152         * the stream. They will be garbage-collected when the stream is garbage-collected or reset.</i>
153         * <li>Adding writeObject/readObject methods - If the version reading the stream has these methods then readObject is
154         * expected, as usual, to read the required data written to the stream by the default serialization. It should call
155         * defaultReadObject first before reading any optional data. The writeObject method is expected as usual to call
156         * defaultWriteObject to write the required data and then may write optional data.</i>
157         * <li>Removing writeObject/readObject methods - If the class reading the stream does not have these methods, the required
158         * data will be read by default serialization, and the optional data will be discarded.</i>
159         * <li>Adding java.io.Serializable - This is equivalent to adding types. There will be no values in the stream for this class
160         * so its fields will be initialized to default values. The support for subclassing nonserializable classes requires that the
161         * class's supertype have a no-arg constructor and the class itself will be initialized to default values. If the no-arg
162         * constructor is not available, the InvalidClassException is thrown.</i>
163         * <li>Changing the access to a field - The access modifiers public, package, protected, and private have no effect on the
164         * ability of serialization to assign values to the fields.</i>
165         * <li>Changing a field from static to nonstatic or transient to nontransient - When relying on default serialization to
166         * compute the serializable fields, this change is equivalent to adding a field to the class. The new field will be written to
167         * the stream but earlier classes will ignore the value since serialization will not assign values to static or transient
168         * fields.</i>
169         * </ul>
170         * All other kinds of modifications should be avoided.
171         * </p>
172         */
173        private static final long serialVersionUID = 1L;
174    
175        private final ReadWriteLock annotationLock = new ReentrantReadWriteLock();
176        private DateTime expirationTimeInUtc;
177        @GuardedBy( "annotationLock" )
178        private Map<Name, Property> annotations = null;
179    
180        /**
181         * Create an empty merge plan
182         */
183        protected MergePlan() {
184        }
185    
186        /**
187         * Determine whether this merge plan has expired given the supplied current time. The {@link #getExpirationTimeInUtc()
188         * expiration time} is the earliest time that any of the {@link #getContributionFrom(String) contributions}
189         * {@link Contribution#getExpirationTimeInUtc()}.
190         * 
191         * @param utcTime the current time expressed in UTC; may not be null
192         * @return true if at least one contribution has expired, or false otherwise
193         */
194        public boolean isExpired( DateTime utcTime ) {
195            assert utcTime != null;
196            assert utcTime.toUtcTimeZone().equals(utcTime); // check that it is passed UTC time
197            return utcTime.isAfter(getExpirationTimeInUtc());
198        }
199    
200        /**
201         * Get the expiration time (in UTC) that is the earliest time that any of the {@link #getContributionFrom(String)
202         * contributions} {@link Contribution#getExpirationTimeInUtc()}.
203         * 
204         * @return the expiration time in UTC, or null if there is no known expiration time
205         */
206        public DateTime getExpirationTimeInUtc() {
207            if (expirationTimeInUtc == null) {
208                // This is computed regardless of a lock, since it's not expensive and idempotent
209                DateTime earliest = null;
210                for (Contribution contribution : this) {
211                    DateTime contributionTime = contribution.getExpirationTimeInUtc();
212                    if (earliest == null || (contributionTime != null && contributionTime.isBefore(earliest))) {
213                        earliest = contributionTime;
214                    }
215                }
216                expirationTimeInUtc = earliest;
217            }
218            return expirationTimeInUtc;
219        }
220    
221        /**
222         * Get the contribution from the source with the supplied name. Note that contributions always include sources that contribute
223         * information and sources that contribute no information. If a source is not included in this list, its contributions are
224         * <i>unknown</i>; that is, it is unknown whether that source does or does not contribute to the node.
225         * 
226         * @param sourceName the name of the source
227         * @return the contribution, or null if the contribution of the source is unknown
228         */
229        public abstract Contribution getContributionFrom( String sourceName );
230    
231        /**
232         * Return whether the named source was consulted for a contribution.
233         * 
234         * @param sourceName the name of the source
235         * @return true if the source has some {@link Contribution contribution} (even if it is an {@link EmptyContribution})
236         */
237        public abstract boolean isSource( String sourceName );
238    
239        public abstract int getContributionCount();
240    
241        /**
242         * Get the plan annotation property with the given name. Plan annotations are custom properties that may be set by
243         * MergeProcessor implementations to store custom properties on the plan. This method does nothing if the supplied name is
244         * null
245         * 
246         * @param name the name of the annotation
247         * @return the existing annotation, or null if there is no annotation with the supplied name
248         * @see #setAnnotation(Property)
249         */
250        public Property getAnnotation( Name name ) {
251            if (name == null) return null;
252            try {
253                annotationLock.readLock().lock();
254                if (this.annotations == null) return null;
255                return this.annotations.get(name);
256            } finally {
257                annotationLock.readLock().unlock();
258            }
259        }
260    
261        /**
262         * Set the plan annotation property. This method replaces and returns any existing annotation property with the same name.
263         * This method also returns immediately if the supplied annotation is null.
264         * 
265         * @param annotation the new annotation
266         * @return the previous annotation property with the same name, or null if there was no previous annotation property for the
267         *         name
268         * @see #getAnnotation(Name)
269         */
270        public Property setAnnotation( Property annotation ) {
271            if (annotation == null) return null;
272            try {
273                annotationLock.writeLock().lock();
274                if (this.annotations == null) {
275                    this.annotations = new HashMap<Name, Property>();
276                }
277                return this.annotations.put(annotation.getName(), annotation);
278            } finally {
279                annotationLock.writeLock().unlock();
280            }
281        }
282    
283        /**
284         * Get the number of annotations.
285         * 
286         * @return the number of annotations
287         */
288        public int getAnnotationCount() {
289            try {
290                annotationLock.readLock().lock();
291                if (this.annotations == null) return 0;
292                return this.annotations.size();
293            } finally {
294                annotationLock.readLock().unlock();
295            }
296        }
297    
298        /**
299         * Get the set of annotation {@link Name names}.
300         * 
301         * @return the unmodifiable set of names, or an empty set if there are no annotations
302         */
303        public Set<Name> getAnnotationNames() {
304            try {
305                annotationLock.readLock().lock();
306                if (this.annotations == null) return Collections.emptySet();
307                return Collections.unmodifiableSet(this.annotations.keySet());
308            } finally {
309                annotationLock.readLock().unlock();
310            }
311        }
312    
313        /**
314         * Set the annotations. This
315         * 
316         * @param annotations
317         */
318        protected void setAnnotations( Map<Name, Property> annotations ) {
319            try {
320                annotationLock.writeLock().lock();
321                this.annotations = annotations == null || annotations.isEmpty() ? null : annotations;
322            } finally {
323                annotationLock.writeLock().unlock();
324            }
325        }
326    
327        /**
328         * Get a copy of the annotations.
329         * 
330         * @return a copy of annotations; never null
331         */
332        public Map<Name, Property> getAnnotations() {
333            Map<Name, Property> result = null;
334            try {
335                annotationLock.writeLock().lock();
336                if (this.annotations != null && !this.annotations.isEmpty()) {
337                    result = new HashMap<Name, Property>(this.annotations);
338                } else {
339                    result = Collections.emptyMap();
340                }
341            } finally {
342                annotationLock.writeLock().unlock();
343            }
344            return result;
345        }
346    
347        /**
348         * {@inheritDoc}
349         * 
350         * @see java.lang.Object#toString()
351         */
352        @Override
353        public String toString() {
354            StringBuilder sb = new StringBuilder();
355            boolean first = true;
356            for (Contribution contribution : this) {
357                if (!first) {
358                    first = false;
359                    sb.append(", ");
360                }
361                sb.append(contribution);
362            }
363            sb.append(getAnnotations());
364            return sb.toString();
365        }
366    
367        /**
368         * {@inheritDoc}
369         * 
370         * @see java.lang.Object#equals(java.lang.Object)
371         */
372        @Override
373        public boolean equals( Object obj ) {
374            if (obj == this) return true;
375            if (obj instanceof MergePlan) {
376                MergePlan that = (MergePlan)obj;
377                if (this.getContributionCount() != that.getContributionCount()) return false;
378                Iterator<Contribution> thisContribution = this.iterator();
379                Iterator<Contribution> thatContribution = that.iterator();
380                while (thisContribution.hasNext() && thatContribution.hasNext()) {
381                    if (!thisContribution.next().equals(thatContribution.next())) return false;
382                }
383                if (this.getAnnotationCount() != that.getAnnotationCount()) return false;
384                if (!this.getAnnotations().equals(that.getAnnotations())) return false;
385                return true;
386            }
387            return false;
388        }
389    
390        protected boolean checkEachContributionIsFromDistinctSource() {
391            Set<String> sourceNames = new HashSet<String>();
392            for (Contribution contribution : this) {
393                boolean added = sourceNames.add(contribution.getSourceName());
394                if (!added) return false;
395            }
396            return true;
397        }
398    
399    }