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 }