ca.ecliptical.pde.internal.ds.DSAnnotationCompilationParticipant.java Source code

Java tutorial

Introduction

Here is the source code for ca.ecliptical.pde.internal.ds.DSAnnotationCompilationParticipant.java

Source

/*******************************************************************************
 * Copyright (c) 2012, 2014 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
 *******************************************************************************/
package ca.ecliptical.pde.internal.ds;

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.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;

import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
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.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.core.ICompilationUnit;
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.build.WorkspaceBuildModel;
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.ds.core.IDSModel;
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_BUILDER = "org.eclipse.pde.ds.core.builder"; //$NON-NLS-1$

    private static final String DS_MANIFEST_KEY = "Service-Component"; //$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,
                true, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        if (!enabled)
            return false;

        if (!PDE.hasPluginNature(project.getProject()))
            return false;

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

        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;

        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.getDefault().getLog().log(e.getStatus());
        }

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

            if (state == null) {
                state = new ProjectState();
                result = NEEDS_FULL_BUILD;
            }

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

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

        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.name(),
                new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
        ValidationErrorLevel errorLevel;
        try {
            errorLevel = ValidationErrorLevel.valueOf(errorLevelStr);
        } catch (IllegalArgumentException e) {
            errorLevel = ValidationErrorLevel.error;
        }

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

        return result;
    }

    private ProjectState loadState(IProject project) throws IOException {
        File workDir = project.getWorkingLocation(Activator.PLUGIN_ID).toFile();
        File stateFile = new File(workDir, STATE_FILENAME);
        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 value = (ProjectState) in.readObject();

            if (debug.isDebugging()) {
                debug.trace(String.format("Loaded state for project: %s", project.getName())); //$NON-NLS-1$
                for (Map.Entry<String, Collection<String>> entry : value.getMappings().entrySet())
                    debug.trace(String.format("%s -> %s", entry.getKey(), entry.getValue())); //$NON-NLS-1$
            }

            return value;
        } catch (ClassNotFoundException e) {
            throw new IOException("Unable to deserialize project state.", e); //$NON-NLS-1$
        } 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
            Map<String, Collection<String>> cuMap = state.getMappings();
            HashSet<String> abandoned = new HashSet<String>(projectContext.getAbandoned());
            for (String cuKey : projectContext.getUnprocessed()) {
                boolean exists = false;
                try {
                    IType cuType = project.findType(cuKey);
                    IResource file;
                    if (cuType != null && (file = cuType.getResource()) != null && file.exists())
                        exists = true;
                } catch (JavaModelException e) {
                    Activator.getDefault().getLog().log(e.getStatus());
                }

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

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

            // remove CUs with no mapped DS models
            HashSet<String> retained = new HashSet<String>();
            for (Iterator<Map.Entry<String, Collection<String>>> i = cuMap.entrySet().iterator(); i.hasNext();) {
                Map.Entry<String, Collection<String>> entry = i.next();
                Collection<String> dsKeys = entry.getValue();
                if (dsKeys.isEmpty())
                    i.remove();
                else
                    retained.addAll(dsKeys);
            }

            // retain abandoned files that are still mapped elsewhere
            abandoned.removeAll(retained);

            if (projectContext.isChanged()) {
                try {
                    saveState(project.getProject(), state);
                } catch (IOException e) {
                    Activator.getDefault().getLog()
                            .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<IStatus>(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.getDefault().getLog()
                        .log(new MultiStatus(Activator.PLUGIN_ID, 0,
                                deleteStatuses.toArray(new IStatus[deleteStatuses.size()]),
                                "Error deleting generated files.", null)); //$NON-NLS-1$

            writeManifest(project.getProject(), retained, abandoned);
            writeBuildProperties(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 workDir = project.getWorkingLocation(Activator.PLUGIN_ID).toFile();
        File stateFile = new File(workDir, STATE_FILENAME);

        if (debug.isDebugging()) {
            debug.trace(String.format("Saving state for project: %s", project.getName())); //$NON-NLS-1$
            for (Map.Entry<String, Collection<String>> entry : state.getMappings().entrySet())
                debug.trace(String.format("%s -> %s", entry.getKey(), entry.getValue())); //$NON-NLS-1$
        }

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

    private void writeManifest(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);
            }
        }, null);
    }

    private void updateManifest(IBundlePluginModelBase model, Collection<String> retained,
            Collection<String> abandoned) {
        IBundleModel bundleModel = model.getBundleModel();
        LinkedHashSet<IPath> entries = new LinkedHashSet<IPath>();
        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.length() == 0 ? null : 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));

        bundleModel.getBundle().setHeader(DS_MANIFEST_KEY, value);
    }

    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.isEmpty())
                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 writeBuildProperties(final IProject project, final Collection<String> retained,
            final Collection<String> abandoned) {
        //      PDEModelUtility.modifyModel(new ModelModification(PDEProject.getBuildProperties(project)) {
        //         @Override
        //         protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
        //            if (model instanceof IBuildModel) {
        IFile file = PDEProject.getBuildProperties(project);
        if (!file.exists())
            return;

        WorkspaceBuildModel wbm = new WorkspaceBuildModel(file);
        wbm.load();
        if (!wbm.isLoaded())
            return;

        try {
            updateBuildProperties(wbm, retained, abandoned);
        } catch (CoreException e) {
            Activator.getDefault().getLog().log(e.getStatus());
        }

        if (wbm.isDirty()) {
            if (debug.isDebugging())
                debug.trace(String.format("Updating %s with %s", file.getFullPath(), //$NON-NLS-1$
                        wbm.getBuild().getEntry(IBuildEntry.BIN_INCLUDES)));

            wbm.save();
        }
        //            }
        //         }
        //      }, null);
    }

    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<IPath>();
            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()).isEmpty())
                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<IJavaProject, Map<ICompilationUnit, BuildContext>>();
        for (BuildContext file : files) {
            ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file.getFile());
            if (cu == null)
                continue;

            Map<ICompilationUnit, BuildContext> map = filesByProject.get(cu.getJavaProject());
            if (map == null) {
                map = new HashMap<ICompilationUnit, BuildContext>();
                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()) {
            processAnnotations(entry.getKey(), entry.getValue());
        }
    }

    private void processAnnotations(IJavaProject javaProject, Map<ICompilationUnit, BuildContext> fileMap) {
        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.none);

        ICompilationUnit[] cuArr = fileMap.keySet().toArray(new ICompilationUnit[fileMap.size()]);
        Map<ICompilationUnit, Collection<IDSModel>> models = new HashMap<ICompilationUnit, Collection<IDSModel>>();
        parser.createASTs(cuArr, new String[0], new AnnotationProcessor(models, fileMap, state.getErrorLevel()),
                null);

        Map<String, Collection<String>> cuMap = state.getMappings();
        Collection<String> unprocessed = projectContext.getUnprocessed();
        Collection<String> abandoned = projectContext.getAbandoned();

        IPath outputPath = new Path(state.getPath()).addTrailingSeparator();

        // save each model to a file; track changes to mappings
        for (Map.Entry<ICompilationUnit, Collection<IDSModel>> entry : models.entrySet()) {
            ICompilationUnit cu = entry.getKey();
            IType cuType = cu.findPrimaryType();
            if (cuType == null) {
                if (debug.isDebugging())
                    debug.trace(String.format("CU %s has no primary type!", cu.getElementName())); //$NON-NLS-1$

                continue; // should never happen
            }

            String cuKey = cuType.getFullyQualifiedName();

            unprocessed.remove(cuKey);
            Collection<String> oldDSKeys = cuMap.remove(cuKey);
            Collection<String> dsKeys = new HashSet<String>();
            cuMap.put(cuKey, dsKeys);

            for (IDSModel model : entry.getValue()) {
                String compName = model.getDSComponent().getAttributeName();
                IPath filePath = outputPath.append(compName).addFileExtension("xml"); //$NON-NLS-1$
                String dsKey = filePath.toPortableString();

                // exclude file from garbage collection
                if (oldDSKeys != null)
                    oldDSKeys.remove(dsKey);

                // add file to CU mapping
                dsKeys.add(dsKey);

                // actually save the file
                IFile compFile = PDEProject.getBundleRelativeFile(javaProject.getProject(), filePath);
                model.setUnderlyingResource(compFile);

                try {
                    ensureDSProject(compFile.getProject());
                } catch (CoreException e) {
                    Activator.getDefault().getLog().log(e.getStatus());
                }

                IPath parentPath = compFile.getParent().getProjectRelativePath();
                if (!parentPath.isEmpty()) {
                    IFolder folder = javaProject.getProject().getFolder(parentPath);
                    try {
                        ensureExists(folder);
                    } catch (CoreException e) {
                        Activator.getDefault().getLog().log(e.getStatus());
                        model.dispose();
                        continue;
                    }
                }

                if (debug.isDebugging())
                    debug.trace(String.format("Saving model: %s", compFile.getFullPath())); //$NON-NLS-1$

                model.save();
                model.dispose();
            }

            // track abandoned files (may be garbage)
            if (oldDSKeys != null)
                abandoned.addAll(oldDSKeys);
        }
    }

    private void ensureDSProject(IProject project) throws CoreException {
        IProjectDescription description = project.getDescription();
        ICommand[] commands = description.getBuildSpec();

        for (ICommand command : commands) {
            if (DS_BUILDER.equals(command.getBuilderName()))
                return;
        }

        ICommand[] newCommands = new ICommand[commands.length + 1];
        System.arraycopy(commands, 0, newCommands, 0, commands.length);
        ICommand command = description.newCommand();
        command.setBuilderName(DS_BUILDER);
        newCommands[newCommands.length - 1] = command;
        description.setBuildSpec(newCommands);
        project.setDescription(description, null);
    }

    private void ensureExists(IFolder folder) throws CoreException {
        if (folder.exists())
            return;

        IContainer parent = folder.getParent();
        if (parent != null && parent.getType() == IResource.FOLDER)
            ensureExists((IFolder) parent);

        folder.create(true, true, null);
    }

    public static boolean isManaged(IProject project) {
        try {
            return project.getSessionProperty(PROP_STATE) != null;
        } catch (CoreException e) {
            return false;
        }
    }
}