/** * Copyright (C) 2005 - 2014 Eric Van Dewoestine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.eclim.plugin.jdt.project; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.eclim.Services; import org.eclim.command.CommandLine; import org.eclim.command.Error; import org.eclim.command.Options; import org.eclim.plugin.core.project.ProjectManager; import org.eclim.plugin.core.util.ProjectUtils; import org.eclim.plugin.core.util.XmlUtils; import org.eclim.plugin.jdt.PluginResources; import org.eclim.plugin.jdt.project.classpath.Dependency; import org.eclim.plugin.jdt.project.classpath.IvyParser; import org.eclim.plugin.jdt.project.classpath.MvnParser; import org.eclim.plugin.jdt.project.classpath.Parser; import org.eclim.plugin.jdt.util.JavaUtils; import org.eclim.util.IOUtils; import org.eclim.util.file.FileOffsets; import org.eclim.util.file.FileUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IClasspathAttribute; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaModelStatus; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaConventions; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.internal.core.JavaProject; import org.eclipse.jdt.internal.ui.wizards.ClassPathDetector; import org.eclipse.jdt.internal.ui.wizards.buildpaths.BuildPathsBlock; import org.eclipse.jdt.internal.ui.wizards.buildpaths.CPListElement; import org.eclipse.jdt.ui.PreferenceConstants; import org.eclipse.jface.preference.IPreferenceStore; /** * Implementation of {@link ProjectManager} for java projects. * * @author Eric Van Dewoestine */ public class JavaProjectManager implements ProjectManager { private static final String PRESERVE = "eclim.preserve"; private static final String CLASSPATH = ".classpath"; private static final String CLASSPATH_XSD = "/resources/schema/eclipse/classpath.xsd"; private static final HashMap<String, Parser> PARSERS = new HashMap<String, Parser>(); static{ PARSERS.put("ivy.xml", new IvyParser()); PARSERS.put("pom.xml", new MvnParser()); } @Override public void create(IProject project, CommandLine commandLine) throws Exception { String depends = commandLine.getValue(Options.DEPENDS_OPTION); create(project, depends); } @Override public List<Error> update(IProject project, CommandLine commandLine) throws Exception { String buildfile = commandLine.getValue(Options.BUILD_FILE_OPTION); IJavaProject javaProject = JavaUtils.getJavaProject(project); javaProject.getResource().refreshLocal(IResource.DEPTH_INFINITE, null); // validate that .classpath xml is well formed and valid. PluginResources resources = (PluginResources) Services.getPluginResources(PluginResources.NAME); List<Error> errors = XmlUtils.validateXml( javaProject.getProject().getName(), CLASSPATH, resources.getResource(CLASSPATH_XSD).toString()); if(errors.size() > 0){ return errors; } String dotclasspath = javaProject.getProject().getFile(CLASSPATH) .getRawLocation().toOSString(); // ivy.xml, pom.xml, etc updated. if(buildfile != null){ try{ IClasspathEntry[] entries = mergeWithBuildfile(javaProject, buildfile); errors = setClasspath(javaProject, entries, dotclasspath); }catch(IllegalStateException ise){ errors.add(new Error(ise.getMessage(), buildfile, 1, 1, false)); } // .classpath updated. }else{ // if an exception occurs reading the classpath then eclipse will return a // default classpath which we would otherwise then write back into the // .classpath file. This hack prevents that and will return a relevent // error message as a validation error. try{ ((JavaProject)javaProject).readFileEntriesWithException(null); } catch(Exception e) { errors.add(new Error(e.getMessage(), dotclasspath, 1, 1, false)); return errors; } IClasspathEntry[] entries = javaProject.readRawClasspath(); errors = setClasspath(javaProject, entries, dotclasspath); } if(errors.size() > 0){ return errors; } return null; } @Override public void refresh(IProject project, CommandLine commandLine) throws Exception { } @Override public void refresh(IProject project, IFile file) throws Exception { } @Override public void delete(IProject project, CommandLine commandLine) throws Exception { } // Project creation methods /** * Creates a new project. * * @param project The project. * @param depends Comma separated project names this project depends on. */ protected void create(IProject project, String depends) throws Exception { // with scala-ide installed, apparently this needs to be explicitly done IProjectDescription desc = project.getDescription(); if(!desc.hasNature(PluginResources.NATURE)){ String[] natures = desc.getNatureIds(); String[] newNatures = new String[natures.length + 1]; System.arraycopy(natures, 0, newNatures, 0, natures.length); newNatures[natures.length] = PluginResources.NATURE; desc.setNatureIds(newNatures); project.setDescription(desc, new NullProgressMonitor()); } IJavaProject javaProject = JavaCore.create(project); ((JavaProject)javaProject).configure(); if (!project.getFile(CLASSPATH).exists()) { ArrayList<IClasspathEntry> classpath = new ArrayList<IClasspathEntry>(); boolean source = false; boolean container = false; ClassPathDetector detector = new ClassPathDetector(project, null); for (IClasspathEntry entry : detector.getClasspath()){ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE){ source = true; } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER){ container = true; } classpath.add(entry); } // default source folder if (!source){ IResource src; IPreferenceStore store = PreferenceConstants.getPreferenceStore(); String name = store.getString(PreferenceConstants.SRCBIN_SRCNAME); boolean srcBinFolders = store.getBoolean( PreferenceConstants.SRCBIN_FOLDERS_IN_NEWPROJ); if (srcBinFolders && name.length() > 0) { src = javaProject.getProject().getFolder(name); } else { src = javaProject.getProject(); } classpath.add( new CPListElement( javaProject, IClasspathEntry.CPE_SOURCE, src.getFullPath(), src) .getClasspathEntry()); File srcPath = new File( ProjectUtils.getFilePath(project, src.getFullPath().toString())); if (!srcPath.exists()){ srcPath.mkdirs(); } } // default containers if (!container){ for (IClasspathEntry entry : PreferenceConstants.getDefaultJRELibrary()){ classpath.add(entry); } } // dependencies on other projects for (IClasspathEntry entry : createOrUpdateDependencies(javaProject, depends)){ classpath.add(entry); } javaProject.setRawClasspath( classpath.toArray(new IClasspathEntry[classpath.size()]), null); // output location IPath output = detector.getOutputLocation(); if (output == null){ output = BuildPathsBlock.getDefaultOutputLocation(javaProject); } javaProject.setOutputLocation(output, null); } javaProject.makeConsistent(null); javaProject.save(null, false); } /** * Creates or updates the projects dependencies on other projects. * * @param project The project. * @param depends The comma seperated list of project names. */ protected IClasspathEntry[] createOrUpdateDependencies( IJavaProject project, String depends) throws Exception { if(depends != null){ String[] dependPaths = StringUtils.split(depends, ','); IClasspathEntry[] entries = new IClasspathEntry[dependPaths.length]; for(int ii = 0; ii < dependPaths.length; ii++){ IProject theProject = ProjectUtils.getProject(dependPaths[ii]); if(!theProject.exists()){ throw new IllegalArgumentException(Services.getMessage( "project.depends.not.found", dependPaths[ii])); } IJavaProject otherProject = JavaCore.create(theProject); entries[ii] = JavaCore.newProjectEntry(otherProject.getPath(), true); } return entries; } return new IClasspathEntry[0]; } /** * Merges the supplied classpath entries into one. * * @param entries The array of classpath entry arrays to merge. * * @return The union of all entry arrays. */ protected IClasspathEntry[] merge(IClasspathEntry[][] entries) { ArrayList<IClasspathEntry> union = new ArrayList<IClasspathEntry>(); if(entries != null){ for(IClasspathEntry[] values : entries){ if(values != null){ for(IClasspathEntry entry : values){ if(!union.contains(entry)){ union.add(entry); } } } } } return (IClasspathEntry[])union.toArray(new IClasspathEntry[union.size()]); } // Project update methods /** * Sets the classpath for the supplied project. * * @param javaProject The project. * @param entries The classpath entries. * @param classpath The file path of the .classpath file. * @return Array of Error or null if no errors reported. */ protected List<Error> setClasspath( IJavaProject javaProject, IClasspathEntry[] entries, String classpath) throws Exception { FileOffsets offsets = FileOffsets.compile(classpath); String classpathValue = IOUtils.toString(new FileInputStream(classpath)); ArrayList<Error> errors = new ArrayList<Error>(); for(IClasspathEntry entry : entries){ IJavaModelStatus status = JavaConventions .validateClasspathEntry(javaProject, entry, true); if(!status.isOK()){ errors.add(createErrorForEntry( javaProject, entry, status, offsets, classpath, classpathValue)); } } IJavaModelStatus status = JavaConventions.validateClasspath( javaProject, entries, javaProject.getOutputLocation()); // always set the classpathValue anyways, so that the user can correct the // file. //if(status.isOK() && errors.isEmpty()){ javaProject.setRawClasspath(entries, null); javaProject.makeConsistent(null); //} if(!status.isOK()){ errors.add(new Error(status.getMessage(), classpath, 1, 1, false)); } return errors; } /** * Creates an Error from the supplied IJavaModelStatus. * * @param project The java project. * @param entry The classpath entry. * @param status The IJavaModelStatus. * @param offsets File offsets for the classpath file. * @param filename The filename of the error. * @param contents The contents of the file as a String. * @return The Error. */ protected Error createErrorForEntry( IJavaProject project, IClasspathEntry entry, IJavaModelStatus status, FileOffsets offsets, String filename, String contents) throws Exception { int line = 0; int col = 0; String path = entry.getPath().toOSString(); path = path.replaceFirst("^/" + project.getProject().getName() + "/", ""); Matcher matcher = Pattern.compile("path\\s*=(['\"])\\s*\\Q" + path + "\\E\\s*\\1") .matcher(contents); if(matcher.find()){ int[] position = offsets.offsetToLineColumn(matcher.start()); line = position[0]; col = position[1]; } return new Error(status.getMessage(), filename, line, col, false); } /** * Merge the supplied project's classpath with entries found in the specified * build file. * * @param project The project. * @param buildfile The path to the build file (pom.xml, ivy.xml, etc) * @return The classpath entries. */ protected IClasspathEntry[] mergeWithBuildfile( IJavaProject project, String buildfile) throws Exception { String filename = FileUtils.getBaseName(buildfile); Parser parser = PARSERS.get(filename); String var = parser.getClasspathVar(); Dependency[] dependencies = parser.parse(buildfile); IWorkspaceRoot root = project.getProject().getWorkspace().getRoot(); ArrayList<IClasspathEntry> results = new ArrayList<IClasspathEntry>(); // load the results with all the non library entries. IClasspathEntry[] entries = project.getRawClasspath(); for(IClasspathEntry entry : entries){ if (entry.getEntryKind() != IClasspathEntry.CPE_LIBRARY && entry.getEntryKind() != IClasspathEntry.CPE_VARIABLE) { results.add(entry); }else{ IPath path = entry.getPath(); String prefix = path != null ? path.segment(0) : null; if ((!var.equals(prefix)) || preserve(entry)){ results.add(entry); } } } // merge the dependencies with the classpath entires. for(int ii = 0; ii < dependencies.length; ii++){ IClasspathEntry match = null; for(int jj = 0; jj < entries.length; jj++){ if (entries[jj].getEntryKind() == IClasspathEntry.CPE_LIBRARY || entries[jj].getEntryKind() == IClasspathEntry.CPE_VARIABLE) { String path = entries[jj].getPath().toOSString(); String pattern = dependencies[ii].getName() + Dependency.VERSION_SEPARATOR; // exact match if(path.endsWith(dependencies[ii].toString())){ match = entries[jj]; results.add(entries[jj]); break; // different version match }else if(path.indexOf(pattern) != -1){ break; } }else if(entries[jj].getEntryKind() == IClasspathEntry.CPE_PROJECT){ String path = entries[jj].getPath().toOSString(); if(path.endsWith(dependencies[ii].getName())){ match = entries[jj]; break; } } } if(match == null){ IClasspathEntry entry = createEntry(root, project, dependencies[ii]); results.add(entry); }else{ match = null; } } return (IClasspathEntry[]) results.toArray(new IClasspathEntry[results.size()]); } /** * Determines if the supplied entry contains attribute indicating that it * should not be removed. * * @param entry The IClasspathEntry * @return true to preserve the entry, false otherwise. */ protected boolean preserve(IClasspathEntry entry) { IClasspathAttribute[] attributes = entry.getExtraAttributes(); for(int ii = 0; ii < attributes.length; ii++){ String name = attributes[ii].getName(); if(PRESERVE.equals(name)){ return Boolean.parseBoolean(attributes[ii].getValue()); } } return false; } /** * Creates the classpath entry. * * @param root The workspace root. * @param project The project to create the dependency in. * @param dependency The dependency to create the entry for. * @return The classpath entry. */ protected IClasspathEntry createEntry( IWorkspaceRoot root, IJavaProject project, Dependency dependency) throws Exception { if(dependency.isVariable()){ return JavaCore.newVariableEntry(dependency.getPath(), null, null, true); } return JavaCore.newLibraryEntry(dependency.getPath(), null, null, true); } /** * Determines if the supplied path starts with a variable name. * * @param path The path to test. * @return True if the path starts with a variable name, false otherwise. */ protected boolean startsWithVariable(String path) { String[] variables = JavaCore.getClasspathVariableNames(); for(int ii = 0; ii < variables.length; ii++){ if(path.startsWith(variables[ii])){ return true; } } return false; } }