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 }