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