org.eclipse.pde.api.tools.internal.builder.BuildState.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.pde.api.tools.internal.builder.BuildState.java

Source

/*******************************************************************************
 * Copyright (c) 2008, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal.builder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.zip.CRC32;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.comparator.Delta;
import org.eclipse.pde.api.tools.internal.model.ProjectComponent;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.comparator.IDelta;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.build.IBuildModel;

/**
 * The API tools build state
 * 
 * @since 1.0.1
 */
public class BuildState {
    private static final IDelta[] EMPTY_DELTAS = new IDelta[0];
    private static final String[] NO_REEXPORTED_COMPONENTS = new String[0];
    private static final int VERSION = 0x20;

    private Map<String, Set<IDelta>> compatibleChanges;
    private Map<String, Set<IDelta>> breakingChanges;
    /**
     * Map of the last saved state of the manifest file
     * 
     * @since 1.0.3
     */
    private Map<String, String> manifestChanges;
    /**
     * Map of the last saved state of the build.properties file
     * 
     * @since 1.0.3
     */
    private Map<String, String> buildPropChanges;
    private String[] reexportedComponents;
    private Set<String> apiToolingDependentProjects;
    private long buildpathCRC = -1L;

    /**
     * Constructor
     */
    BuildState() {
        this.compatibleChanges = new HashMap<String, Set<IDelta>>();
        this.breakingChanges = new HashMap<String, Set<IDelta>>();
        this.manifestChanges = new HashMap<String, String>();
        this.buildPropChanges = new HashMap<String, String>();
    }

    /**
     * Reads the build state from an input stream
     * 
     * @param in
     * @return the {@link BuildState} from the given input stream
     * @throws IOException
     */
    public static BuildState read(DataInputStream in) throws IOException {
        String pluginID = in.readUTF();
        if (!pluginID.equals(ApiPlugin.PLUGIN_ID)) {
            throw new IOException(BuilderMessages.build_wrongFileFormat);
        }
        String kind = in.readUTF();
        if (!kind.equals("STATE")) {//$NON-NLS-1$
            throw new IOException(BuilderMessages.build_wrongFileFormat);
        }
        if (in.readInt() != VERSION) {
            // this is an old build state - a full build is required
            return null;
        }
        if (in.readBoolean()) {
            // continue to read
            BuildState state = new BuildState();
            state.buildpathCRC = in.readLong();
            int count = in.readInt();
            // read all compatible deltas
            for (int i = 0; i < count; i++) {
                state.addCompatibleChange(readDelta(in));
            }
            count = in.readInt();
            // read all breaking deltas
            for (int i = 0; i < count; i++) {
                state.addBreakingChange(readDelta(in));
            }
            count = in.readInt();
            // read all re-exported component names
            String[] components = new String[count];
            for (int i = 0; i < count; i++) {
                components[i] = in.readUTF();
            }
            state.reexportedComponents = components;
            count = in.readInt();
            for (int i = 0; i < count; i++) {
                state.addApiToolingDependentProject(in.readUTF());
            }
            if (in.available() > 0) {
                count = in.readInt();
                if (count > 0) {
                    // read the saved headers
                    HashMap<String, String> map = new HashMap<String, String>(count);
                    for (int i = 0; i < count; i++) {
                        String key = in.readUTF();
                        String value = in.readUTF();
                        map.put(key, value);
                    }
                    state.setManifestState(map);
                }
                count = in.readInt();
                if (count > 0) {
                    // read the saved headers
                    HashMap<String, String> map = new HashMap<String, String>(count);
                    for (int i = 0; i < count; i++) {
                        String key = in.readUTF();
                        String value = in.readUTF();
                        map.put(key, value);
                    }
                    state.setBuildPropertiesState(map);
                }
            }
            return state;
        }
        return null;
    }

    /**
     * Writes the given {@link BuildState} to the given output stream
     * 
     * @param state
     * @param out
     * @throws IOException
     */
    public static void write(BuildState state, DataOutputStream out) throws IOException {
        out.writeUTF(ApiPlugin.PLUGIN_ID);
        out.writeUTF("STATE"); //$NON-NLS-1$
        out.writeInt(VERSION);
        out.writeBoolean(true);
        out.writeLong(state.buildpathCRC);
        IDelta[] compatibleChangesDeltas = state.getCompatibleChanges();
        int length = compatibleChangesDeltas.length;
        out.writeInt(length);
        for (int i = 0; i < length; i++) {
            writeDelta(compatibleChangesDeltas[i], out);
        }
        IDelta[] breakingChangesDeltas = state.getBreakingChanges();
        length = breakingChangesDeltas.length;
        out.writeInt(length);
        for (int i = 0; i < length; i++) {
            writeDelta(breakingChangesDeltas[i], out);
        }
        String[] reexportedComponents = state.getReexportedComponents();
        length = reexportedComponents.length;
        out.writeInt(length);
        for (int i = 0; i < length; i++) {
            out.writeUTF(reexportedComponents[i]);
        }
        Set<String> apiToolingDependentsProjects = state.getApiToolingDependentProjects();
        length = apiToolingDependentsProjects.size();
        out.writeInt(length);
        for (Iterator<String> iterator = apiToolingDependentsProjects.iterator(); iterator.hasNext();) {
            out.writeUTF(iterator.next());
        }
        Map<String, String> map = state.getManifestState();
        out.writeInt(map.size());
        Entry<String, String> entry = null;
        for (Iterator<Entry<String, String>> i = map.entrySet().iterator(); i.hasNext();) {
            entry = i.next();
            out.writeUTF(entry.getKey());
            out.writeUTF(entry.getValue());
        }
        map = state.getBuildPropertiesState();
        out.writeInt(map.size());
        entry = null;
        for (Iterator<Entry<String, String>> i = map.entrySet().iterator(); i.hasNext();) {
            entry = i.next();
            out.writeUTF(entry.getKey());
            out.writeUTF(entry.getValue());
        }
    }

    /**
     * Read the {@link IDelta} from the build state (input stream)
     * 
     * @param in the input stream to read the {@link IDelta} from
     * @return a reconstructed {@link IDelta} from the build state
     * @throws IOException
     */
    private static IDelta readDelta(DataInputStream in) throws IOException {
        // decode the delta from the build state
        boolean hasComponentID = in.readBoolean();
        String componentID = null;
        if (hasComponentID) {
            in.readUTF(); // delta.getComponentID()
        }
        int elementType = in.readInt(); // delta.getElementType()
        int kind = in.readInt(); // delta.getKind()
        int flags = in.readInt(); // delta.getFlags()
        int restrictions = in.readInt(); // delta.getRestrictions()
        int modifiers = in.readInt(); // delta.getModifiers()
        String typeName = in.readUTF(); // delta.getTypeName()
        String key = in.readUTF(); // delta.getKey()
        int length = in.readInt(); // arguments.length;
        String[] datas = null;
        if (length != 0) {
            ArrayList<String> arguments = new ArrayList<String>();
            for (int i = 0; i < length; i++) {
                arguments.add(in.readUTF());
            }
            datas = new String[length];
            arguments.toArray(datas);
        } else {
            datas = new String[1];
            datas[0] = typeName.replace('$', '.');
        }
        int oldModifiers = modifiers & Delta.MODIFIERS_MASK;
        int newModifiers = modifiers >>> Delta.NEW_MODIFIERS_OFFSET;
        int previousRestrictions = restrictions >>> Delta.PREVIOUS_RESTRICTIONS_OFFSET;
        int currentRestrictions = restrictions & Delta.RESTRICTIONS_MASK;
        return new Delta(componentID, elementType, kind, flags, currentRestrictions, previousRestrictions,
                oldModifiers, newModifiers, typeName, key, datas);
    }

    /**
     * Writes a given {@link IDelta} to the build state (the output stream)
     * 
     * @param delta the delta to write
     * @param out the stream to write to
     * @throws IOException
     */
    private static void writeDelta(IDelta delta, DataOutputStream out) throws IOException {
        // encode a delta into the build state
        // int elementType, int kind, int flags, int restrictions, int
        // modifiers, String typeName, String key, Object data
        String apiComponentID = delta.getComponentVersionId();
        boolean hasComponentID = apiComponentID != null;
        out.writeBoolean(hasComponentID);
        if (hasComponentID) {
            out.writeUTF(apiComponentID);
        }
        out.writeInt(delta.getElementType());
        out.writeInt(delta.getKind());
        out.writeInt(delta.getFlags());
        out.writeInt(delta.getCurrentRestrictions());
        int modifiers = (delta.getNewModifiers() << Delta.NEW_MODIFIERS_OFFSET) | delta.getOldModifiers();
        out.writeInt(modifiers);
        out.writeUTF(delta.getTypeName());
        out.writeUTF(delta.getKey());
        String[] arguments = delta.getArguments();
        int length = arguments.length;
        out.writeInt(length);
        for (int i = 0; i < length; i++) {
            out.writeUTF(arguments[i]);
        }
    }

    /**
     * Adds an {@link IDelta} for a compatible compatibility change to the
     * current state
     * 
     * @param delta the {@link IDelta} to add to the state
     */
    public void addCompatibleChange(IDelta delta) {
        String typeName = delta.getTypeName();
        Set<IDelta> object = this.compatibleChanges.get(typeName);
        if (object == null) {
            Set<IDelta> changes = new HashSet<IDelta>();
            changes.add(delta);
            this.compatibleChanges.put(typeName, changes);
        } else {
            object.add(delta);
        }
    }

    /**
     * Add an {@link IDelta} for an incompatible compatibility change to the
     * current state
     * 
     * @param delta the {@link IDelta} to add to the state
     */
    public void addBreakingChange(IDelta delta) {
        String typeName = delta.getTypeName();
        Set<IDelta> object = this.breakingChanges.get(typeName);
        if (object == null) {
            Set<IDelta> changes = new HashSet<IDelta>();
            changes.add(delta);
            this.breakingChanges.put(typeName, changes);
        } else {
            object.add(delta);
        }
    }

    /**
     * @return the complete list of recorded breaking changes with duplicates
     *         removed, or an empty array, never <code>null</code>
     */
    public IDelta[] getBreakingChanges() {
        if (this.breakingChanges == null || this.breakingChanges.size() == 0) {
            return EMPTY_DELTAS;
        }
        HashSet<IDelta> collector = new HashSet<IDelta>();
        Collection<Set<IDelta>> values = this.breakingChanges.values();
        for (Iterator<Set<IDelta>> iterator = values.iterator(); iterator.hasNext();) {
            collector.addAll(iterator.next());
        }
        return collector.toArray(new IDelta[collector.size()]);
    }

    /**
     * @return the complete list of recorded compatible changes with duplicates
     *         removed, or an empty array, never <code>null</code>
     */
    public IDelta[] getCompatibleChanges() {
        if (this.compatibleChanges == null || this.compatibleChanges.size() == 0) {
            return EMPTY_DELTAS;
        }
        HashSet<IDelta> collector = new HashSet<IDelta>();
        Collection<Set<IDelta>> values = this.compatibleChanges.values();
        for (Iterator<Set<IDelta>> iterator = values.iterator(); iterator.hasNext();) {
            collector.addAll(iterator.next());
        }
        return collector.toArray(new IDelta[collector.size()]);
    }

    /**
     * @return the complete list of re-exported {@link IApiComponent}s
     */
    public String[] getReexportedComponents() {
        if (this.reexportedComponents == null) {
            return NO_REEXPORTED_COMPONENTS;
        }
        return this.reexportedComponents;
    }

    /**
     * Remove all entries for the given type name.
     * 
     * @param typeName the given type name
     */
    public void cleanup(String typeName) {
        this.breakingChanges.remove(typeName);
        this.compatibleChanges.remove(typeName);
        this.reexportedComponents = null;
    }

    /**
     * Sets the current list if re-exported {@link IApiComponent}s for this
     * build state
     * 
     * @param components
     */
    public void setReexportedComponents(IApiComponent[] components) {
        if (components == null) {
            return;
        }
        if (this.reexportedComponents == null) {
            final int length = components.length;
            String[] result = new String[length];
            for (int i = 0; i < length; i++) {
                result[i] = components[i].getSymbolicName();
            }
            this.reexportedComponents = result;
        }
    }

    /**
     * Adds a dependent project to the listing of dependent projects
     * 
     * @param projectName
     */
    public void addApiToolingDependentProject(String projectName) {
        if (this.apiToolingDependentProjects == null) {
            this.apiToolingDependentProjects = new HashSet<String>(3);
        }
        this.apiToolingDependentProjects.add(projectName);
    }

    /**
     * @return the complete listing of dependent projects
     */
    public Set<String> getApiToolingDependentProjects() {
        return this.apiToolingDependentProjects == null ? Collections.EMPTY_SET : this.apiToolingDependentProjects;
    }

    /**
     * Allows the last built state of the manifest to be saved. This method will
     * perform compaction of the manifest, removing headers that we not need to
     * care about.
     * 
     * @param state the last built state of the manifest
     * @since 1.0.3
     */
    public void setManifestState(Map<String, String> state) {
        if (state != null) {
            Map<String, String> compact = new HashMap<String, String>(7);
            for (Iterator<String> i = ApiAnalysisBuilder.IMPORTANT_HEADERS.iterator(); i.hasNext();) {
                String key = i.next();
                String val = state.get(key);
                if (val != null) {
                    compact.put(key, val);
                }
            }
            this.manifestChanges = compact;
        } else {
            this.manifestChanges.clear();
        }
    }

    /**
     * Returns the last saved state of the manifest or an empty {@link Map},
     * never <code>null</code>
     * 
     * @return the last built state of the manifest or an empty {@link Map},
     *         never <code>null</code>
     * @since 1.0.3
     */
    public Map<String, String> getManifestState() {
        return this.manifestChanges;
    }

    /**
     * Allows the last built state of the build.properties file to be saved.
     * This method will only save entries that we care about, not an entire
     * build.properties file snap-shot. <br>
     * <br>
     * The retained entries are:
     * <ul>
     * <li>names that match: <code>custom</code></li>
     * <li>names that start with: {@link IBuildEntry#JAR_PREFIX}</li>
     * <li>names that start with: <code>extra.</code></li>
     * </ul>
     * 
     * @param model the {@link IBuildModel} to save
     * @since 1.0.3
     */
    public void setBuildPropertiesState(IBuildModel model) {
        if (model != null) {
            IBuildEntry[] entries = model.getBuild().getBuildEntries();
            String name = null;
            for (int i = 0; i < entries.length; i++) {
                name = entries[i].getName();
                if (ProjectComponent.ENTRY_CUSTOM.equals(name)) {
                    this.buildPropChanges.put(ProjectComponent.ENTRY_CUSTOM,
                            Util.deepToString(entries[i].getTokens()));
                } else if (name.startsWith(IBuildEntry.JAR_PREFIX)) {
                    this.buildPropChanges.put(name, Util.deepToString(entries[i].getTokens()));
                } else if (name.startsWith(ProjectComponent.EXTRA_PREFIX)) {
                    this.buildPropChanges.put(name, Util.deepToString(entries[i].getTokens()));
                }
            }
        } else {
            this.buildPropChanges.clear();
        }
    }

    /**
     * Allows the map to be reset to the given map, passing in <code>null</code>
     * clears the current mapping.
     * 
     * @param map the map to set
     * @since 1.0.3
     */
    void setBuildPropertiesState(Map<String, String> map) {
        if (map != null) {
            this.buildPropChanges = map;
        } else {
            this.buildPropChanges.clear();
        }
    }

    /**
     * Returns the last built state of the build.properties file or an empty
     * {@link Map}, never <code>null</code>
     * 
     * @return the last built state of the build.properties file or an empty
     *         {@link Map}, never <code>null</code>
     * @since 1.0.3
     */
    public Map<String, String> getBuildPropertiesState() {
        return this.buildPropChanges;
    }

    /**
     * Returns a CRC32 code of the project's build path or -1 if unknown.
     * 
     * @return CRC32 code of the project's build path or -1
     */
    public long getBuildPathCRC() {
        return buildpathCRC;
    }

    /**
     * Sets the build path CRC for this project's resolved build path.
     * 
     * @param crc32 crc32 code
     */
    public void setBuildPathCRC(long crc32) {
        buildpathCRC = crc32;
    }

    /**
     * Return the last built state for the given project, or null if none
     */
    public static BuildState getLastBuiltState(IProject project) throws CoreException {
        if (!Util.isApiProject(project)) {
            // should never be requested on non-Java projects
            return null;
        }
        return readState(project);
    }

    /**
     * Reads the build state for the relevant project.
     * 
     * @return the current {@link BuildState} for the given project or
     *         <code>null</code> if there is not one
     */
    static BuildState readState(IProject project) throws CoreException {
        File file = getSerializationFile(project);
        if (file != null && file.exists()) {
            try {
                DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
                try {
                    return read(in);
                } finally {
                    if (ApiPlugin.DEBUG_BUILDER) {
                        System.out.println("ApiAnalysisBuilder: Saved state thinks last build failed for " //$NON-NLS-1$
                                + project.getName());
                    }
                    in.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
                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$
            }
        } else if (ApiPlugin.DEBUG_BUILDER) {
            if (file == null) {
                System.out.println("ApiAnalysisBuilder: Project does not exist: " + project); //$NON-NLS-1$
            } else {
                System.out.println("ApiAnalysisBuilder: Build state file " + file.getPath() + " does not exist"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }
        return null;
    }

    /**
     * Sets the last built state for the given project, or null to reset it.
     * 
     * @param project the project to set a state for
     * @param state the {@link BuildState} to set as the last state
     */
    public static void setLastBuiltState(IProject project, BuildState state) throws CoreException {
        if (Util.isApiProject(project)) {
            // should never be requested on non-Java projects
            if (state != null) {
                saveBuiltState(project, state);
            } else {
                try {
                    File file = getSerializationFile(project);
                    if (file != null && file.exists()) {
                        file.delete();
                    }
                } catch (SecurityException se) {
                    // could not delete file: cannot do much more
                }
            }
        }
    }

    /**
     * Returns the {@link File} to use for saving and restoring the last built
     * state for the given project.
     * 
     * @param project gets the saved state file for the given project
     * @return the {@link File} to use for saving and restoring the last built
     *         state for the given project.
     */
    static File getSerializationFile(IProject project) {
        if (!project.exists()) {
            return null;
        }
        IPath workingLocation = project.getWorkingLocation(ApiPlugin.PLUGIN_ID);
        return workingLocation.append("state.dat").toFile(); //$NON-NLS-1$
    }

    /**
     * Saves the current build state
     * 
     * @param project
     * @param state
     * @throws CoreException
     */
    static void saveBuiltState(IProject project, BuildState state) throws CoreException {
        if (ApiPlugin.DEBUG_BUILDER) {
            System.out.println("ApiAnalysisBuilder: Saving build state for project: " + project.getName()); //$NON-NLS-1$
        }
        File file = BuildState.getSerializationFile(project);
        if (file == null) {
            return;
        }
        long t = 0;
        if (ApiPlugin.DEBUG_BUILDER) {
            t = System.currentTimeMillis();
        }
        try {
            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
            try {
                write(state, out);
            } finally {
                out.close();
            }
        } catch (RuntimeException e) {
            try {
                file.delete();
            } catch (SecurityException se) {
                // could not delete file: cannot do much more
            }
            throw new CoreException(new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, Platform.PLUGIN_ERROR,
                    NLS.bind(BuilderMessages.build_cannotSaveState, project.getName()), e));
        } catch (IOException e) {
            try {
                file.delete();
            } catch (SecurityException se) {
                // could not delete file: cannot do much more
            }
            throw new CoreException(new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, Platform.PLUGIN_ERROR,
                    NLS.bind(BuilderMessages.build_cannotSaveState, project.getName()), e));
        }
        if (ApiPlugin.DEBUG_BUILDER) {
            t = System.currentTimeMillis() - t;
            System.out.println(NLS.bind(BuilderMessages.build_saveStateComplete, String.valueOf(t)));
        }
    }

    /**
     * Computes and returns a CRC of the projects resolved build path, or -1 if
     * unknown.
     * 
     * @param project project
     * @return build path CRC or -1
     */
    public static long computeBuildPathCRC(IProject project) {
        IJavaProject jp = JavaCore.create(project);
        try {
            IClasspathEntry[] classpath = jp.getResolvedClasspath(true);
            CRC32 crc32 = new CRC32();
            for (int i = 0; i < classpath.length; i++) {
                IClasspathEntry entry = classpath[i];
                crc32.update(entry.getPath().toPortableString().getBytes());
            }
            return crc32.getValue();
        } catch (JavaModelException e) {
        }
        return -1L;
    }
}