| 1 | /******************************************************************************* | 
| 2 |  * Copyright (c) 2008, 2009 IBM Corporation and others. | 
| 3 |  * All rights reserved. This program and the accompanying materials | 
| 4 |  * are made available under the terms of the Eclipse Public License v1.0 | 
| 5 |  * which accompanies this distribution, and is available at | 
| 6 |  * http://www.eclipse.org/legal/epl-v10.html | 
| 7 |  *  | 
| 8 |  * Contributors: | 
| 9 |  *     IBM Corporation - initial API and implementation | 
| 10 |  *******************************************************************************/ | 
| 11 | package org.eclipse.pde.api.tools.internal.builder; | 
| 12 |   | 
| 13 | import java.io.BufferedInputStream; | 
| 14 | import java.io.BufferedOutputStream; | 
| 15 | import java.io.DataInputStream; | 
| 16 | import java.io.DataOutputStream; | 
| 17 | import java.io.File; | 
| 18 | import java.io.FileInputStream; | 
| 19 | import java.io.FileOutputStream; | 
| 20 | import java.io.IOException; | 
| 21 | import java.util.ArrayList; | 
| 22 | import java.util.Collection; | 
| 23 | import java.util.Collections; | 
| 24 | import java.util.HashMap; | 
| 25 | import java.util.HashSet; | 
| 26 | import java.util.Iterator; | 
| 27 | import java.util.Map; | 
| 28 | import java.util.Set; | 
| 29 |   | 
| 30 | import org.eclipse.core.resources.IProject; | 
| 31 | import org.eclipse.core.runtime.CoreException; | 
| 32 | import org.eclipse.core.runtime.IPath; | 
| 33 | import org.eclipse.core.runtime.IStatus; | 
| 34 | import org.eclipse.core.runtime.Platform; | 
| 35 | import org.eclipse.core.runtime.Status; | 
| 36 | import org.eclipse.jdt.core.JavaCore; | 
| 37 | import org.eclipse.osgi.util.NLS; | 
| 38 | import org.eclipse.pde.api.tools.internal.comparator.Delta; | 
| 39 | import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; | 
| 40 | import org.eclipse.pde.api.tools.internal.provisional.comparator.IDelta; | 
| 41 | import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent; | 
| 42 | import org.eclipse.pde.api.tools.internal.util.Util; | 
| 43 |   | 
| 44 | /** | 
| 45 |  * The API tools build state | 
| 46 |  *  | 
| 47 |  * @since 1.0.1 | 
| 48 |  */ | 
| 49 | public class BuildState { | 
| 50 |         private static final IDelta[] EMPTY_DELTAS = new IDelta[0]; | 
| 51 |         private static final String[] NO_REEXPORTED_COMPONENTS = new String[0]; | 
| 52 |         private static final int VERSION = 0x10; | 
| 53 |          | 
| 54 |         private Map compatibleChanges; | 
| 55 |         private Map breakingChanges; | 
| 56 |         private String[] reexportedComponents; | 
| 57 |         private Set apiToolingDependentProjects; | 
| 58 |          | 
| 59 |         /** | 
| 60 |          * Constructor | 
| 61 |          */ | 
| 62 |         BuildState() { | 
| 63 |                 this.compatibleChanges = new HashMap(); | 
| 64 |                 this.breakingChanges = new HashMap(); | 
| 65 |         } | 
| 66 |          | 
| 67 |         /** | 
| 68 |          * Reads the build state from an input stream | 
| 69 |          * @param in | 
| 70 |          * @return the {@link BuildState} from the given input stream | 
| 71 |          * @throws IOException | 
| 72 |          */ | 
| 73 |          public static BuildState read(DataInputStream in) throws IOException { | 
| 74 |                 String pluginID= in.readUTF(); | 
| 75 |                 if (!pluginID.equals(ApiPlugin.PLUGIN_ID)) { | 
| 76 |                         throw new IOException(BuilderMessages.build_wrongFileFormat); | 
| 77 |                 } | 
| 78 |                 String kind= in.readUTF(); | 
| 79 |                 if (!kind.equals("STATE")) {//$NON-NLS-1$ | 
| 80 |                         throw new IOException(BuilderMessages.build_wrongFileFormat); | 
| 81 |                 } | 
| 82 |                 if (in.readInt() != VERSION) { | 
| 83 |                         // this is an old build state - a full build is required | 
| 84 |                         return null; | 
| 85 |                 } | 
| 86 |                 if (in.readBoolean()) { | 
| 87 |                         // continue to read | 
| 88 |                         BuildState state = new BuildState(); | 
| 89 |                         int numberOfCompatibleDeltas = in.readInt(); | 
| 90 |                         // read all compatible deltas | 
| 91 |                         for (int i = 0; i < numberOfCompatibleDeltas; i++) { | 
| 92 |                                 state.addCompatibleChange(readDelta(in)); | 
| 93 |                         } | 
| 94 |                         int numberOfBreakingDeltas = in.readInt(); | 
| 95 |                         // read all breaking deltas | 
| 96 |                         for (int i = 0; i < numberOfBreakingDeltas; i++) { | 
| 97 |                                 state.addBreakingChange(readDelta(in)); | 
| 98 |                         } | 
| 99 |                         int numberOfReexportedComponents = in.readInt(); | 
| 100 |                         // read all reexported component names | 
| 101 |                         String[] components = new String[numberOfReexportedComponents]; | 
| 102 |                         for (int i = 0; i < numberOfReexportedComponents; i++) { | 
| 103 |                                 components[i] = in.readUTF(); | 
| 104 |                         } | 
| 105 |                         state.reexportedComponents = components; | 
| 106 |                         int numberOfApiToolingDependents = in.readInt(); | 
| 107 |                         for (int i = 0; i < numberOfApiToolingDependents; i++) { | 
| 108 |                                 state.addApiToolingDependentProject(in.readUTF()); | 
| 109 |                         } | 
| 110 |                         return state; | 
| 111 |                 } | 
| 112 |                 return null; | 
| 113 |         } | 
| 114 |           | 
| 115 |         /** | 
| 116 |          * Writes the given {@link BuildState} to the given output stream | 
| 117 |          * @param state | 
| 118 |          * @param out | 
| 119 |          * @throws IOException | 
| 120 |          */ | 
| 121 |         public static void write(BuildState state, DataOutputStream out) throws IOException { | 
| 122 |                 out.writeUTF(ApiPlugin.PLUGIN_ID); | 
| 123 |                 out.writeUTF("STATE"); //$NON-NLS-1$ | 
| 124 |                 out.writeInt(VERSION); | 
| 125 |                 out.writeBoolean(true); | 
| 126 |                 IDelta[] compatibleChangesDeltas = state.getCompatibleChanges(); | 
| 127 |                 int length = compatibleChangesDeltas.length; | 
| 128 |                 out.writeInt(length); | 
| 129 |                 for (int i = 0; i < length; i++) { | 
| 130 |                         writeDelta(compatibleChangesDeltas[i], out); | 
| 131 |                 } | 
| 132 |                 IDelta[] breakingChangesDeltas = state.getBreakingChanges(); | 
| 133 |                 length = breakingChangesDeltas.length; | 
| 134 |                 out.writeInt(length); | 
| 135 |                 for (int i = 0; i < length; i++) { | 
| 136 |                         writeDelta(breakingChangesDeltas[i], out); | 
| 137 |                 } | 
| 138 |                 String[] reexportedComponents = state.getReexportedComponents(); | 
| 139 |                 length = reexportedComponents.length; | 
| 140 |                 out.writeInt(length); | 
| 141 |                 for (int i = 0; i < length; i++) { | 
| 142 |                         out.writeUTF(reexportedComponents[i]); | 
| 143 |                 } | 
| 144 |                 Set apiToolingDependentsProjects = state.getApiToolingDependentProjects(); | 
| 145 |                 length = apiToolingDependentsProjects.size(); | 
| 146 |                 out.writeInt(length); | 
| 147 |                 for (Iterator iterator = apiToolingDependentsProjects.iterator(); iterator.hasNext(); ) { | 
| 148 |                         out.writeUTF((String) iterator.next()); | 
| 149 |                 } | 
| 150 |         } | 
| 151 |          | 
| 152 |         /** | 
| 153 |          * Read the {@link IDelta} from the build state (input stream) | 
| 154 |          * @param in the input stream to read the {@link IDelta} from | 
| 155 |          * @return a reconstructed {@link IDelta} from the build state | 
| 156 |          * @throws IOException | 
| 157 |          */ | 
| 158 |         private static IDelta readDelta(DataInputStream in) throws IOException { | 
| 159 |                 // decode the delta from the build state | 
| 160 |                 boolean hasComponentID = in.readBoolean(); | 
| 161 |                 String componentID = null; | 
| 162 |                 if (hasComponentID) in.readUTF(); // delta.getComponentID() | 
| 163 |                 int elementType = in.readInt(); // delta.getElementType() | 
| 164 |                 int kind = in.readInt(); // delta.getKind() | 
| 165 |                 int flags = in.readInt(); // delta.getFlags() | 
| 166 |                 int restrictions = in.readInt(); // delta.getRestrictions() | 
| 167 |                 int modifiers = in.readInt(); // delta.getModifiers() | 
| 168 |                 String typeName = in.readUTF(); // delta.getTypeName() | 
| 169 |                 String key = in.readUTF(); // delta.getKey() | 
| 170 |                 int length = in.readInt(); // arguments.length; | 
| 171 |                 String[] datas = null; | 
| 172 |                 if (length != 0) { | 
| 173 |                         ArrayList arguments = new ArrayList(); | 
| 174 |                         for (int i = 0; i < length; i++) { | 
| 175 |                                 arguments.add(in.readUTF()); | 
| 176 |                         } | 
| 177 |                         datas = new String[length]; | 
| 178 |                         arguments.toArray(datas); | 
| 179 |                 } else { | 
| 180 |                         datas = new String[1]; | 
| 181 |                         datas[0] = typeName.replace('$', '.'); | 
| 182 |                 } | 
| 183 |                 int oldModifiers = modifiers & Delta.MODIFIERS_MASK; | 
| 184 |                 int newModifiers = modifiers >>> Delta.NEW_MODIFIERS_OFFSET; | 
| 185 |                 return new Delta(componentID, elementType, kind, flags, restrictions, oldModifiers, newModifiers, typeName, key, datas); | 
| 186 |         } | 
| 187 |          | 
| 188 |         /** | 
| 189 |          * Writes a given {@link IDelta} to the build state (the output stream) | 
| 190 |          * @param delta the delta to write | 
| 191 |          * @param out the stream to write to | 
| 192 |          * @throws IOException | 
| 193 |          */ | 
| 194 |         private static void writeDelta(IDelta delta, DataOutputStream out) throws IOException { | 
| 195 |                 // encode a delta into the build state | 
| 196 |                 // int elementType, int kind, int flags, int restrictions, int modifiers, String typeName, String key, Object data | 
| 197 |                 String apiComponentID = delta.getComponentVersionId(); | 
| 198 |                 boolean hasComponentID = apiComponentID != null; | 
| 199 |                 out.writeBoolean(hasComponentID); | 
| 200 |                 if (hasComponentID) { | 
| 201 |                         out.writeUTF(apiComponentID); | 
| 202 |                 } | 
| 203 |                 out.writeInt(delta.getElementType()); | 
| 204 |                 out.writeInt(delta.getKind()); | 
| 205 |                 out.writeInt(delta.getFlags()); | 
| 206 |                 out.writeInt(delta.getRestrictions()); | 
| 207 |                 int modifiers = (delta.getNewModifiers() << Delta.NEW_MODIFIERS_OFFSET) | delta.getOldModifiers(); | 
| 208 |                 out.writeInt(modifiers); | 
| 209 |                 out.writeUTF(delta.getTypeName()); | 
| 210 |                 out.writeUTF(delta.getKey()); | 
| 211 |                 String[] arguments = delta.getArguments(); | 
| 212 |                 int length = arguments.length; | 
| 213 |                 out.writeInt(length); | 
| 214 |                 for (int i = 0; i < length; i++) { | 
| 215 |                         out.writeUTF(arguments[i]); | 
| 216 |                 } | 
| 217 |         } | 
| 218 |   | 
| 219 |         /** | 
| 220 |          * Adds an {@link IDelta} for a compatible compatibility change to the current state | 
| 221 |          *  | 
| 222 |          * @param delta the {@link IDelta} to add to the state | 
| 223 |          */ | 
| 224 |         public void addCompatibleChange(IDelta delta) { | 
| 225 |                 String typeName = delta.getTypeName(); | 
| 226 |                 Set object = (Set) this.compatibleChanges.get(typeName); | 
| 227 |                 if (object == null) { | 
| 228 |                         Set changes = new HashSet(); | 
| 229 |                         changes.add(delta); | 
| 230 |                         this.compatibleChanges.put(typeName, changes); | 
| 231 |                 } else { | 
| 232 |                         object.add(delta); | 
| 233 |                 } | 
| 234 |         } | 
| 235 |   | 
| 236 |         /** | 
| 237 |          * Add an {@link IDelta} for an incompatible compatibility change to the current state | 
| 238 |          *  | 
| 239 |          * @param delta the {@link IDelta} to add to the state | 
| 240 |          */ | 
| 241 |         public void addBreakingChange(IDelta delta) { | 
| 242 |                 String typeName = delta.getTypeName(); | 
| 243 |                 Set object = (Set) this.breakingChanges.get(typeName); | 
| 244 |                 if (object == null) { | 
| 245 |                         Set changes = new HashSet(); | 
| 246 |                         changes.add(delta); | 
| 247 |                         this.breakingChanges.put(typeName, changes); | 
| 248 |                 } else { | 
| 249 |                         object.add(delta); | 
| 250 |                 } | 
| 251 |         } | 
| 252 |          | 
| 253 |         /** | 
| 254 |          * @return the complete list of recorded breaking changes with duplicates removed, or  | 
| 255 |          * an empty array, never <code>null</code> | 
| 256 |          */ | 
| 257 |         public IDelta[] getBreakingChanges() { | 
| 258 |                 if (this.breakingChanges == null || this.breakingChanges.size() == 0) { | 
| 259 |                         return EMPTY_DELTAS; | 
| 260 |                 } | 
| 261 |                 HashSet collector = new HashSet(); | 
| 262 |                 Collection values = this.breakingChanges.values(); | 
| 263 |                 for (Iterator iterator = values.iterator(); iterator.hasNext(); ) { | 
| 264 |                         collector.addAll((HashSet) iterator.next()); | 
| 265 |                 } | 
| 266 |                 return (IDelta[]) collector.toArray(new IDelta[collector.size()]); | 
| 267 |         } | 
| 268 |   | 
| 269 |         /** | 
| 270 |          * @return the complete list of recorded compatible changes with duplicates removed, | 
| 271 |          * or an empty array, never <code>null</code> | 
| 272 |          */ | 
| 273 |         public IDelta[] getCompatibleChanges() { | 
| 274 |                 if (this.compatibleChanges == null || this.compatibleChanges.size() == 0) { | 
| 275 |                         return EMPTY_DELTAS; | 
| 276 |                 } | 
| 277 |                 HashSet collector = new HashSet(); | 
| 278 |                 Collection values = this.compatibleChanges.values(); | 
| 279 |                 for (Iterator iterator = values.iterator(); iterator.hasNext(); ) { | 
| 280 |                         collector.addAll((HashSet) iterator.next()); | 
| 281 |                 } | 
| 282 |                 return (IDelta[]) collector.toArray(new IDelta[collector.size()]); | 
| 283 |         } | 
| 284 |   | 
| 285 |         /** | 
| 286 |          * @return the complete list of re-exported {@link IApiComponent}s | 
| 287 |          */ | 
| 288 |         public String[] getReexportedComponents() { | 
| 289 |                 if (this.reexportedComponents == null) { | 
| 290 |                         return NO_REEXPORTED_COMPONENTS; | 
| 291 |                 } | 
| 292 |                 return this.reexportedComponents; | 
| 293 |         } | 
| 294 |          | 
| 295 |         /** | 
| 296 |          * Remove all entries for the given type name. | 
| 297 |          * | 
| 298 |          * @param typeName the given type name | 
| 299 |          */ | 
| 300 |         public void cleanup(String typeName) { | 
| 301 |                 this.breakingChanges.remove(typeName); | 
| 302 |                 this.compatibleChanges.remove(typeName); | 
| 303 |                 this.reexportedComponents = null; | 
| 304 |         } | 
| 305 |   | 
| 306 |         /** | 
| 307 |          * Sets the current list if re-exported {@link IApiComponent}s for this build state | 
| 308 |          * @param components | 
| 309 |          */ | 
| 310 |         public void setReexportedComponents(IApiComponent[] components) { | 
| 311 |                 if (components == null) { | 
| 312 |                         return; | 
| 313 |                 } | 
| 314 |                 if (this.reexportedComponents == null) { | 
| 315 |                         final int length = components.length; | 
| 316 |                         String[] result = new String[length]; | 
| 317 |                         for (int i = 0; i < length; i++) { | 
| 318 |                                 result[i] = components[i].getId(); | 
| 319 |                         } | 
| 320 |                         this.reexportedComponents = result; | 
| 321 |                 } | 
| 322 |         } | 
| 323 |   | 
| 324 |         /** | 
| 325 |          * Adds a dependent project to the listing of dependent projects | 
| 326 |          * @param projectName | 
| 327 |          */ | 
| 328 |         public void addApiToolingDependentProject(String projectName) { | 
| 329 |                 if (this.apiToolingDependentProjects == null) { | 
| 330 |                         this.apiToolingDependentProjects = new HashSet(3); | 
| 331 |                 } | 
| 332 |                 this.apiToolingDependentProjects.add(projectName); | 
| 333 |         } | 
| 334 |          | 
| 335 |         /** | 
| 336 |          * @return the complete listing of dependent projects | 
| 337 |          */ | 
| 338 |         public Set getApiToolingDependentProjects() { | 
| 339 |                 return this.apiToolingDependentProjects == null ? Collections.EMPTY_SET : this.apiToolingDependentProjects; | 
| 340 |         } | 
| 341 |         /** | 
| 342 |          * Return the last built state for the given project, or null if none | 
| 343 |          */ | 
| 344 |         public static BuildState getLastBuiltState(IProject project) throws CoreException { | 
| 345 |                 if (!Util.isApiProject(project)) { | 
| 346 |                         // should never be requested on non-Java projects | 
| 347 |                         return null; | 
| 348 |                 } | 
| 349 |                 return readState(project); | 
| 350 |         } | 
| 351 |          | 
| 352 |         /** | 
| 353 |          * Reads the build state for the relevant project. | 
| 354 |          * @return the current {@link BuildState} for the given project or <code>null</code> if there is not one | 
| 355 |          */ | 
| 356 |         static BuildState readState(IProject project) throws CoreException { | 
| 357 |                 File file = getSerializationFile(project); | 
| 358 |                 if (file != null && file.exists()) { | 
| 359 |                         try { | 
| 360 |                                 DataInputStream in= new DataInputStream(new BufferedInputStream(new FileInputStream(file))); | 
| 361 |                                 try { | 
| 362 |                                         return read(in); | 
| 363 |                                 } finally { | 
| 364 |                                         if (ApiAnalysisBuilder.DEBUG) { | 
| 365 |                                                 System.out.println("Saved state thinks last build failed for " + project.getName()); //$NON-NLS-1$ | 
| 366 |                                         } | 
| 367 |                                         in.close(); | 
| 368 |                                 } | 
| 369 |                         } catch (Exception e) { | 
| 370 |                                 e.printStackTrace(); | 
| 371 |                                 throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, Platform.PLUGIN_ERROR, "Error reading last build state for project "+ project.getName(), e)); //$NON-NLS-1$ | 
| 372 |                         } | 
| 373 |                 } else if (ApiAnalysisBuilder.DEBUG) { | 
| 374 |                         if (file == null) { | 
| 375 |                                 System.out.println("Project does not exist: " + project); //$NON-NLS-1$ | 
| 376 |                         } else { | 
| 377 |                                 System.out.println("Build state file " + file.getPath() + " does not exist"); //$NON-NLS-1$ //$NON-NLS-2$ | 
| 378 |                         } | 
| 379 |                 } | 
| 380 |                 return null; | 
| 381 |         } | 
| 382 |          | 
| 383 |         /** | 
| 384 |          * Sets the last built state for the given project, or null to reset it. | 
| 385 |          *  | 
| 386 |          * @param project the project to set a state for | 
| 387 |          * @param state the {@link BuildState} to set as the last state | 
| 388 |          */ | 
| 389 |         public static void setLastBuiltState(IProject project, BuildState state) throws CoreException { | 
| 390 |                 if (Util.isApiProject(project)) { | 
| 391 |                         // should never be requested on non-Java projects | 
| 392 |                         if (state != null) { | 
| 393 |                                 saveBuiltState(project, state); | 
| 394 |                         } else { | 
| 395 |                                 try { | 
| 396 |                                         File file = getSerializationFile(project); | 
| 397 |                                         if (file != null && file.exists()) { | 
| 398 |                                                 file.delete(); | 
| 399 |                                         } | 
| 400 |                                 } catch(SecurityException se) { | 
| 401 |                                         // could not delete file: cannot do much more | 
| 402 |                                 } | 
| 403 |                         } | 
| 404 |                 } | 
| 405 |         } | 
| 406 |          | 
| 407 |         /** | 
| 408 |          * Returns the {@link File} to use for saving and restoring the last built state for the given project. | 
| 409 |          *  | 
| 410 |          * @param project gets the saved state file for the given project | 
| 411 |          * @return the {@link File} to use for saving and restoring the last built state for the given project. | 
| 412 |          */ | 
| 413 |         static File getSerializationFile(IProject project) { | 
| 414 |                 if (!project.exists()) { | 
| 415 |                         return null; | 
| 416 |                 } | 
| 417 |                 IPath workingLocation = project.getWorkingLocation(ApiPlugin.PLUGIN_ID); | 
| 418 |                 return workingLocation.append("state.dat").toFile(); //$NON-NLS-1$ | 
| 419 |         } | 
| 420 |          | 
| 421 |         /** | 
| 422 |          * Saves the current build state | 
| 423 |          * @param project | 
| 424 |          * @param state | 
| 425 |          * @throws CoreException | 
| 426 |          */ | 
| 427 |         static void saveBuiltState(IProject project, BuildState state) throws CoreException { | 
| 428 |                 if (ApiAnalysisBuilder.DEBUG) { | 
| 429 |                         System.out.println("Saving build state for project: "+project.getName()); //$NON-NLS-1$ | 
| 430 |                 } | 
| 431 |                 File file = BuildState.getSerializationFile(project); | 
| 432 |                 if (file == null) return; | 
| 433 |                 long t = 0; | 
| 434 |                 if (ApiAnalysisBuilder.DEBUG) { | 
| 435 |                         t = System.currentTimeMillis(); | 
| 436 |                 } | 
| 437 |                 try { | 
| 438 |                         DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); | 
| 439 |                         try { | 
| 440 |                                 write(state, out); | 
| 441 |                         } finally { | 
| 442 |                                 out.close(); | 
| 443 |                         } | 
| 444 |                 } catch (RuntimeException e) { | 
| 445 |                         try { | 
| 446 |                                 file.delete(); | 
| 447 |                         } catch(SecurityException se) { | 
| 448 |                                 // could not delete file: cannot do much more | 
| 449 |                         } | 
| 450 |                         throw new CoreException( | 
| 451 |                                 new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, Platform.PLUGIN_ERROR, | 
| 452 |                                         NLS.bind(BuilderMessages.build_cannotSaveState, project.getName()), e));  | 
| 453 |                 } catch (IOException e) { | 
| 454 |                         try { | 
| 455 |                                 file.delete(); | 
| 456 |                         } catch(SecurityException se) { | 
| 457 |                                 // could not delete file: cannot do much more | 
| 458 |                         } | 
| 459 |                         throw new CoreException( | 
| 460 |                                 new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, Platform.PLUGIN_ERROR, | 
| 461 |                                         NLS.bind(BuilderMessages.build_cannotSaveState, project.getName()), e));  | 
| 462 |                 } | 
| 463 |                 if (ApiAnalysisBuilder.DEBUG) { | 
| 464 |                         t = System.currentTimeMillis() - t; | 
| 465 |                         System.out.println(NLS.bind(BuilderMessages.build_saveStateComplete, String.valueOf(t)));  | 
| 466 |                 } | 
| 467 |         } | 
| 468 | } |