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 }