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 }