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 }