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 | } |