org.eclipse.pde.ds.internal.annotations.DSAnnotationCompilationParticipant.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.pde.ds.internal.annotations.DSAnnotationCompilationParticipant.java

Source

/*******************************************************************************
 * Copyright (c) 2012, 2016 Ecliptical Software Inc. 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:
 *     Ecliptical Software Inc. - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 490062
 *******************************************************************************/
package org.eclipse.pde.ds.internal.annotations;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.BuildContext;
import org.eclipse.jdt.core.compiler.CompilationParticipant;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.build.IBuildModel;
import org.eclipse.pde.core.build.IBuildModelFactory;
import org.eclipse.pde.internal.core.WorkspaceModelManager;
import org.eclipse.pde.internal.core.ibundle.IBundleModel;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
import org.eclipse.pde.internal.core.natures.PDE;
import org.eclipse.pde.internal.core.project.PDEProject;
import org.eclipse.pde.internal.ui.util.ModelModification;
import org.eclipse.pde.internal.ui.util.PDEModelUtility;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.component.annotations.Component;

@SuppressWarnings("restriction")
public class DSAnnotationCompilationParticipant extends CompilationParticipant {

    private static final String DS_MANIFEST_KEY = "Service-Component"; //$NON-NLS-1$

    private static final String AP_MANIFEST_KEY = "Bundle-ActivationPolicy"; //$NON-NLS-1$

    static final String COMPONENT_ANNOTATION = Component.class.getName();

    private static final QualifiedName PROP_STATE = new QualifiedName(Activator.PLUGIN_ID, "state"); //$NON-NLS-1$

    private static final String STATE_FILENAME = "state.dat"; //$NON-NLS-1$

    private static final Debug debug = Debug.getDebug("ds-annotation-builder"); //$NON-NLS-1$

    private final Map<IJavaProject, ProjectContext> processingContext = Collections
            .synchronizedMap(new HashMap<IJavaProject, ProjectContext>());

    @Override
    public boolean isAnnotationProcessor() {
        return true;
    }

    @Override
    public boolean isActive(IJavaProject project) {
        boolean enabled = Platform.getPreferencesService().getBoolean(Activator.PLUGIN_ID, Activator.PREF_ENABLED,
                false, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE,
                        DefaultScope.INSTANCE });
        if (!enabled)
            return false;

        IProject iproject = project.getProject();
        if (!iproject.isOpen() || !PDE.hasPluginNature(iproject))
            return false;

        if (WorkspaceModelManager.isBinaryProject(project.getProject()))
            return false;

        try {
            IType annotationType = project.findType(COMPONENT_ANNOTATION);
            return annotationType != null && annotationType.isAnnotation();
        } catch (JavaModelException e) {
            Activator.log(e);
        }

        return false;
    }

    @Override
    public int aboutToBuild(IJavaProject project) {
        if (debug.isDebugging())
            debug.trace(String.format("About to build project: %s", project.getElementName())); //$NON-NLS-1$

        int result = READY_FOR_BUILD;

        int[] retval = new int[1];
        ProjectState state = getState(project, retval);
        result = retval[0];

        processingContext.put(project, new ProjectContext(state));

        if (state.getFormatVersion() != ProjectState.FORMAT_VERSION) {
            state.setFormatVersion(ProjectState.FORMAT_VERSION);
            result = NEEDS_FULL_BUILD;
        }

        String path = Platform.getPreferencesService().getString(Activator.PLUGIN_ID, Activator.PREF_PATH,
                Activator.DEFAULT_PATH,
                new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        if (!path.equals(state.getPath())) {
            state.setPath(path);
            result = NEEDS_FULL_BUILD;
        }

        String errorLevelStr = Platform.getPreferencesService().getString(Activator.PLUGIN_ID,
                Activator.PREF_VALIDATION_ERROR_LEVEL, ValidationErrorLevel.error.toString(),
                new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        ValidationErrorLevel errorLevel = getEnumValue(errorLevelStr, ValidationErrorLevel.class,
                ValidationErrorLevel.error);

        if (errorLevel != state.getErrorLevel()) {
            state.setErrorLevel(errorLevel);
            result = NEEDS_FULL_BUILD;
        }

        String missingUnbindMethodLevelStr = Platform.getPreferencesService().getString(Activator.PLUGIN_ID,
                Activator.PREF_MISSING_UNBIND_METHOD_ERROR_LEVEL, errorLevelStr,
                new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        ValidationErrorLevel missingUnbindMethodLevel = getEnumValue(missingUnbindMethodLevelStr,
                ValidationErrorLevel.class, errorLevel);

        if (missingUnbindMethodLevel != state.getMissingUnbindMethodLevel()) {
            state.setMissingUnbindMethodLevel(missingUnbindMethodLevel);
            result = NEEDS_FULL_BUILD;
        }

        return result;
    }

    private <E extends Enum<E>> E getEnumValue(String property, Class<E> enumType, E defaultValue) {
        try {
            return Enum.valueOf(enumType, property);
        } catch (IllegalArgumentException e) {
            return defaultValue;
        }
    }

    public static ProjectState getState(IJavaProject project) {
        return getState(project, null);
    }

    private static ProjectState getState(IJavaProject project, int[] result) {
        ProjectState state = null;
        try {
            Object value = project.getProject().getSessionProperty(PROP_STATE);
            if (value instanceof SoftReference<?>) {
                @SuppressWarnings("unchecked")
                SoftReference<ProjectState> ref = (SoftReference<ProjectState>) value;
                state = ref.get();
            }
        } catch (CoreException e) {
            Activator.log(e);
        }

        if (state == null) {
            try {
                state = loadState(project.getProject());
            } catch (IOException e) {
                Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error loading project state.", e)); //$NON-NLS-1$
            }

            if (state == null) {
                state = new ProjectState();
                if (result != null && result.length > 0)
                    result[0] = NEEDS_FULL_BUILD;
            }

            try {
                project.getProject().setSessionProperty(PROP_STATE, new SoftReference<>(state));
            } catch (CoreException e) {
                Activator.log(e);
            }
        }

        return state;
    }

    private static ProjectState loadState(IProject project) throws IOException {
        File stateFile = getStateFile(project);
        if (!stateFile.canRead()) {
            if (debug.isDebugging())
                debug.trace(String.format("Missing or invalid project state file: %s", stateFile)); //$NON-NLS-1$

            return null;
        }

        ObjectInputStream in = new ObjectInputStream(new FileInputStream(stateFile));
        try {
            ProjectState state = (ProjectState) in.readObject();

            if (debug.isDebugging()) {
                debug.trace(String.format("Loaded state for project: %s", project.getName())); //$NON-NLS-1$
                for (String cuKey : state.getCompilationUnits())
                    debug.trace(String.format("%s -> %s", cuKey, state.getModelFiles(cuKey))); //$NON-NLS-1$
            }

            return state;
        } catch (ClassNotFoundException e) {
            IOException ex = new IOException("Unable to deserialize project state."); //$NON-NLS-1$
            ex.initCause(e);
            throw ex;
        } finally {
            in.close();
        }
    }

    @Override
    public void buildFinished(IJavaProject project) {
        ProjectContext projectContext = processingContext.remove(project);
        if (projectContext != null) {
            ProjectState state = projectContext.getState();
            // check if unprocessed CUs still exist; if not, their mapped files are now abandoned
            HashSet<String> abandoned = new HashSet<>(projectContext.getAbandoned());
            for (String cuKey : projectContext.getUnprocessed()) {
                boolean exists = false;
                try {
                    IJavaElement cu = project.findElement(new Path(cuKey));
                    IResource file;
                    if (cu != null && cu.getElementType() == IJavaElement.COMPILATION_UNIT
                            && (file = cu.getResource()) != null && file.exists())
                        exists = true;
                } catch (JavaModelException e) {
                    Activator.log(e);
                }

                if (!exists) {
                    if (debug.isDebugging())
                        debug.trace(String.format("Mapped CU %s no longer exists.", cuKey)); //$NON-NLS-1$

                    Collection<String> dsKeys = state.removeMappings(cuKey);
                    if (dsKeys != null)
                        abandoned.addAll(dsKeys);
                }
            }

            // retain abandoned files that are still mapped elsewhere
            HashSet<String> retained = new HashSet<>();
            for (String cuKey : state.getCompilationUnits()) {
                Collection<String> dsKeys = state.getModelFiles(cuKey);
                if (dsKeys != null)
                    retained.addAll(dsKeys);
            }

            abandoned.removeAll(retained);

            if (projectContext.isChanged()) {
                try {
                    saveState(project.getProject(), state);
                } catch (IOException e) {
                    Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error saving file mappings.", e)); //$NON-NLS-1$
                }
            }

            // delete all abandoned files
            ArrayList<IStatus> deleteStatuses = new ArrayList<>(2);
            for (String dsKey : abandoned) {
                IPath path = Path.fromPortableString(dsKey);

                if (debug.isDebugging())
                    debug.trace(String.format("Deleting %s", path)); //$NON-NLS-1$

                IFile file = PDEProject.getBundleRelativeFile(project.getProject(), path);
                if (file.exists()) {
                    try {
                        file.delete(true, null);
                    } catch (CoreException e) {
                        deleteStatuses.add(e.getStatus());
                    }
                }
            }

            if (!deleteStatuses.isEmpty())
                Activator.log(new MultiStatus(Activator.PLUGIN_ID, 0,
                        deleteStatuses.toArray(new IStatus[deleteStatuses.size()]),
                        "Error deleting generated files.", null)); //$NON-NLS-1$

            if (!retained.isEmpty() || !abandoned.isEmpty())
                updateProject(project.getProject(), retained, abandoned);
        }

        if (debug.isDebugging())
            debug.trace(String.format("Build finished for project: %s", project.getElementName())); //$NON-NLS-1$
    }

    private void saveState(IProject project, ProjectState state) throws IOException {
        File stateFile = getStateFile(project);

        if (debug.isDebugging()) {
            debug.trace(String.format("Saving state for project: %s", project.getName())); //$NON-NLS-1$
            for (String cuKey : state.getCompilationUnits())
                debug.trace(String.format("%s -> %s", cuKey, state.getModelFiles(cuKey))); //$NON-NLS-1$
        }

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(stateFile));
        try {
            out.writeObject(state);
        } finally {
            out.close();
        }
    }

    private void updateProject(IProject project, final Collection<String> retained,
            final Collection<String> abandoned) {
        PDEModelUtility.modifyModel(new ModelModification(project) {
            @Override
            protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
                if (model instanceof IBundlePluginModelBase)
                    updateManifest((IBundlePluginModelBase) model, retained, abandoned, project);
            }
        }, null);

        // note: we can't combine both manifest and build.properties into a single edit
        PDEModelUtility.modifyModel(new ModelModification(PDEProject.getBuildProperties(project)) {
            @Override
            protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
                if (model instanceof IBuildModel)
                    updateBuildProperties((IBuildModel) model, retained, abandoned);
            }
        }, null);
    }

    private void updateManifest(IBundlePluginModelBase model, Collection<String> retained,
            Collection<String> abandoned, IProject project) {
        IBundleModel bundleModel = model.getBundleModel();
        LinkedHashSet<IPath> entries = new LinkedHashSet<>();
        collectManifestEntries(bundleModel, entries);

        boolean changed = false;
        for (String dsKey : abandoned) {
            IPath path = Path.fromPortableString(dsKey);
            changed |= entries.remove(path);
        }

        for (String dsKey : retained) {
            IPath path = Path.fromPortableString(dsKey);
            if (!isManifestEntryIncluded(entries, path))
                changed |= entries.add(path);
        }

        if (!changed)
            return;

        StringBuilder buf = new StringBuilder();
        for (IPath entry : entries) {
            if (buf.length() > 0)
                buf.append(",\n "); //$NON-NLS-1$

            buf.append(entry.toString());
        }

        String value = buf.toString();

        if (debug.isDebugging())
            debug.trace(String.format("Setting manifest header in %s to %s: %s", //$NON-NLS-1$
                    model.getUnderlyingResource().getFullPath(), DS_MANIFEST_KEY, value));

        // note: contrary to javadoc, setting header value to null does *not* remove it; setting it to empty string does
        bundleModel.getBundle().setHeader(DS_MANIFEST_KEY, value);

        boolean generateBAPL = Platform.getPreferencesService().getBoolean(Activator.PLUGIN_ID,
                Activator.PREF_GENERATE_BAPL, true,
                new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        if (generateBAPL) {
            if (debug.isDebugging())
                debug.trace(String.format("Setting manifest header in %s to %s: %s", //$NON-NLS-1$
                        model.getUnderlyingResource().getFullPath(), AP_MANIFEST_KEY, "lazy")); //$NON-NLS-1$

            bundleModel.getBundle().setHeader(AP_MANIFEST_KEY, "lazy"); //$NON-NLS-1$
        }
    }

    private void collectManifestEntries(IBundleModel bundleModel, Collection<IPath> entries) {
        String header = bundleModel.getBundle().getHeader(DS_MANIFEST_KEY);
        if (header == null)
            return;

        String[] elements = header.split("\\s*,\\s*"); //$NON-NLS-1$
        for (String element : elements) {
            if (element.length() != 0)
                entries.add(new Path(element));
        }
    }

    private boolean isManifestEntryIncluded(Collection<IPath> entries, IPath path) {
        for (IPath entry : entries) {
            if (entry.equals(path))
                return true;

            if (entry.removeLastSegments(1).equals(path.removeLastSegments(1))) {
                // check if wildcard match (last path segment)
                Filter filter;
                try {
                    filter = FrameworkUtil
                            .createFilter("(filename=" + sanitizeFilterValue(entry.lastSegment()) + ")"); //$NON-NLS-1$ //$NON-NLS-2$
                } catch (InvalidSyntaxException e) {
                    continue;
                }

                if (filter.matches(Collections.singletonMap("filename", path.lastSegment()))) //$NON-NLS-1$
                    return true;
            }
        }

        return false;
    }

    private String sanitizeFilterValue(String value) {
        return value.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
    }

    private void updateBuildProperties(IBuildModel model, Collection<String> retained, Collection<String> abandoned)
            throws CoreException {
        IBuildEntry includes = model.getBuild().getEntry(IBuildEntry.BIN_INCLUDES);

        if (includes != null) {
            for (String dsKey : abandoned) {
                String path = Path.fromPortableString(dsKey).toString();
                if (includes.contains(path))
                    includes.removeToken(path);
            }
        }

        if (!retained.isEmpty()) {
            if (includes == null) {
                IBuildModelFactory factory = model.getFactory();
                includes = factory.createEntry(IBuildEntry.BIN_INCLUDES);
                model.getBuild().add(includes);
            }

            LinkedHashSet<IPath> entries = new LinkedHashSet<>();
            collectBuildEntries(includes, entries);

            for (String dsKey : retained) {
                IPath path = Path.fromPortableString(dsKey);
                if (!isBuildEntryIncluded(entries, path))
                    includes.addToken(path.toString());
            }
        }
    }

    private void collectBuildEntries(IBuildEntry includes, Collection<IPath> entries) {
        if (includes == null)
            return;

        for (String include : includes.getTokens()) {
            if ((include = include.trim()).length() != 0)
                entries.add(new Path(include));
        }
    }

    private boolean isBuildEntryIncluded(Collection<IPath> entries, IPath path) {
        for (IPath entry : entries) {
            if (entry.equals(path))
                return true;

            if (entry.hasTrailingSeparator() && entry.isPrefixOf(path))
                return true;

            // TODO support full Ant path patterns
        }

        return false;
    }

    @Override
    public void processAnnotations(BuildContext[] files) {
        // we need to process CUs in context of a project; separate them by project
        HashMap<IJavaProject, Map<ICompilationUnit, BuildContext>> filesByProject = new HashMap<>();
        for (BuildContext file : files) {
            if (debug.isDebugging())
                debug.trace(String.format("Creating compilation unit from file %s.", file.getFile().getFullPath())); //$NON-NLS-1$

            ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file.getFile());
            if (cu == null) {
                if (debug.isDebugging())
                    // TODO should we log instead? Don't want to spam the error log though
                    debug.trace(String.format("Unable to create compilation unit from file %s.", //$NON-NLS-1$
                            file.getFile().getFullPath()));

                continue;
            }

            Map<ICompilationUnit, BuildContext> map = filesByProject.get(cu.getJavaProject());
            if (map == null) {
                map = new HashMap<>();
                filesByProject.put(cu.getJavaProject(), map);
            }

            map.put(cu, file);
        }

        // process all CUs in each project
        for (Map.Entry<IJavaProject, Map<ICompilationUnit, BuildContext>> entry : filesByProject.entrySet()) {
            if (debug.isDebugging())
                debug.trace(String.format("Processing compilation units in project %s.", //$NON-NLS-1$
                        entry.getKey().getElementName()));

            processAnnotations(entry.getKey(), entry.getValue());
        }
    }

    private void processAnnotations(IJavaProject javaProject, Map<ICompilationUnit, BuildContext> fileMap) {
        @SuppressWarnings("deprecation")
        ASTParser parser = ASTParser.newParser(AST.JLS4);
        parser.setResolveBindings(true);
        parser.setBindingsRecovery(true);
        parser.setProject(javaProject);
        parser.setKind(ASTParser.K_COMPILATION_UNIT);

        ProjectContext projectContext = processingContext.get(javaProject);
        ProjectState state = projectContext.getState();

        parser.setIgnoreMethodBodies(state.getErrorLevel() == ValidationErrorLevel.ignore);

        ICompilationUnit[] cuArr = fileMap.keySet().toArray(new ICompilationUnit[fileMap.size()]);
        parser.createASTs(cuArr, new String[0], new AnnotationProcessor(projectContext, fileMap), null);
    }

    public static boolean isManaged(IProject project) {
        try {
            if (project.getSessionProperty(PROP_STATE) != null)
                return true;

            File stateFile = getStateFile(project);
            return stateFile.canRead();
        } catch (CoreException e) {
            return false;
        }
    }

    private static File getStateFile(IProject project) {
        File workDir = project.getWorkingLocation(Activator.PLUGIN_ID).toFile();
        File stateFile = new File(workDir, STATE_FILENAME);
        return stateFile;
    }
}