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