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 }