/** * 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.util; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.apache.commons.vfs.FileObject; import org.apache.commons.vfs.FileSystemManager; import org.apache.commons.vfs.VFS; import org.eclim.Services; import org.eclim.plugin.core.preference.Preferences; import org.eclim.plugin.core.project.ProjectNatureFactory; import org.eclim.plugin.core.util.ProjectUtils; import org.eclim.plugin.jdt.PluginResources; import org.eclim.util.IOUtils; import org.eclim.util.file.FileUtils; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.core.IBuffer; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IImportDeclaration; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IPackageDeclaration; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.BodyDeclaration; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Javadoc; import org.eclipse.jdt.core.formatter.IndentManipulation; import org.eclipse.jdt.internal.core.DocumentAdapter; import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility; import org.eclipse.jdt.internal.corext.refactoring.structure.ASTNodeSearchUtil; import org.eclipse.jdt.internal.formatter.DefaultCodeFormatter; import org.eclipse.jdt.internal.ui.JavaPlugin; import org.eclipse.jdt.internal.ui.text.correction.ContributedProcessorDescriptor; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jdt.ui.text.java.IQuickAssistProcessor; import org.eclipse.jdt.ui.text.java.IQuickFixProcessor; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; /** * Utility methods for working with java files / projects. * * @author Eric Van Dewoestine */ public class JavaUtils { /** * String version of java.lang package. */ public static final String JAVA_LANG = "java.lang"; private static final Pattern PACKAGE_LINE = Pattern.compile("^\\s*package\\s+(\\w+(\\.\\w+){0,})\\s*;\\s*$"); private static final Pattern TRAILING_WHITESPACE = Pattern.compile("[ \t]+$", Pattern.MULTILINE); private static ContributedProcessorDescriptor[] correctionProcessors; private static ContributedProcessorDescriptor[] assistProcessors; /** * Gets a java project by name. * * @param project The name of the project. * @return The project. */ public static IJavaProject getJavaProject(String project) throws Exception { return getJavaProject(ProjectUtils.getProject(project, true)); } /** * Gets a java project from the supplied IProject. * * @param project The IProject. * @return The java project. */ public static IJavaProject getJavaProject(IProject project) throws Exception { if(ProjectUtils.getPath(project) == null){ throw new IllegalArgumentException( Services.getMessage("project.location.null", project.getName())); } if(!project.hasNature(PluginResources.NATURE)){ throw new IllegalArgumentException(Services.getMessage( "project.missing.nature", project.getName(), ProjectNatureFactory.getAliasForNature(PluginResources.NATURE))); } IJavaProject javaProject = JavaCore.create(project); if(javaProject == null || !javaProject.exists()){ throw new IllegalArgumentException( Services.getMessage("project.not.found", project)); } return javaProject; } /** * Finds a compilation unit by looking in the java project of the supplied * name. * * @param project The name of the project to locate the file in. * @param file The file to find. * @return The compilation unit. * * @throws IllegalArgumentException If the file is not found. */ public static ICompilationUnit getCompilationUnit(String project, String file) throws Exception { IJavaProject javaProject = getJavaProject(project); return getCompilationUnit(javaProject, file); } /** * Finds a compilation unit by looking in all the available java projects. * * @param file The absolute file path to find. * @return The compilation unit. * * @throws IllegalArgumentException If the file is not found. */ public static ICompilationUnit getCompilationUnit(String file) throws Exception { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for(int ii = 0; ii < projects.length; ii++){ IJavaProject javaProject = getJavaProject(projects[ii]); ICompilationUnit src = JavaCore.createCompilationUnitFrom( ProjectUtils.getFile(javaProject.getProject(), file)); if(src != null && src.exists()){ return src; } } throw new IllegalArgumentException( Services.getMessage("src.file.not.found", file, ".classpath")); } /** * Gets the compilation unit from the supplied project. * * @param project The project. * @param file The absolute path to the file. * @return The compilation unit or null if not found. * * @throws IllegalArgumentException If the file is not found. */ public static ICompilationUnit getCompilationUnit( IJavaProject project, String file) throws Exception { ICompilationUnit src = JavaCore.createCompilationUnitFrom( ProjectUtils.getFile(project.getProject(), file)); if(src == null || !src.exists()){ throw new IllegalArgumentException( Services.getMessage("src.file.not.found", file, ".classpath")); } return src; } /** * Finds a compilation unit by looking in all the java project of the supplied * name. * * @param project The name of the project to locate the file in. * @param file The src dir relative file path to find. * @return The compilation unit or null if not found. */ public static ICompilationUnit findCompilationUnit( String project, String file) throws Exception { IPath path = Path.fromOSString(file); IJavaProject javaProject = getJavaProject(project); javaProject.open(null); //javaProject.getResource().refreshLocal(IResource.DEPTH_INFINITE, null); ICompilationUnit src = (ICompilationUnit)javaProject.findElement(path); return src; } /** * Finds a compilation unit by looking in all the available java projects. * * @param file The src directory relative file to find. * @return The compilation unit or null if not found. */ public static ICompilationUnit findCompilationUnit(String file) throws Exception { IPath path = Path.fromOSString(file); IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for(IProject project : projects){ if (project.hasNature(JavaCore.NATURE_ID)){ IJavaProject javaProject = getJavaProject(project); javaProject.open(null); //javaProject.getResource().refreshLocal(IResource.DEPTH_INFINITE, null); ICompilationUnit src = (ICompilationUnit)javaProject.findElement(path); if(src != null){ return src; } } } return null; } /** * Attempts to locate the IClassFile for the supplied file path from the * specified project's classpath. * * @param project The project to find the class file in. * @param path Absolute path or url (jar:, zip:) to a .java source file. * @return The IClassFile. */ public static IClassFile findClassFile(IJavaProject project, String path) throws Exception { if(path.startsWith("/") || path.toLowerCase().startsWith("jar:") || path.toLowerCase().startsWith("zip:")) { FileSystemManager fsManager = VFS.getManager(); FileObject file = fsManager.resolveFile(path.replace("%", "%25")); if(file.exists()){ BufferedReader in = null; try{ in = new BufferedReader( new InputStreamReader(file.getContent().getInputStream())); String pack = null; String line = null; while((line = in.readLine()) != null){ Matcher matcher = PACKAGE_LINE.matcher(line); if (matcher.matches()){ pack = matcher.group(1); break; } } if (pack != null){ String name = pack + '.' + FileUtils.getFileName(file.getName().getPath()); IType type = project.findType(name); if (type != null){ return type.getClassFile(); } } }finally{ IOUtils.closeQuietly(in); } } } return null; } /** * Gets the primary element (compilation unit or class file) for the supplied * element. * * @param element The element. * @return The primary element. */ public static IJavaElement getPrimaryElement(IJavaElement element) { IJavaElement parent = element; while(parent.getElementType() != IJavaElement.COMPILATION_UNIT && parent.getElementType() != IJavaElement.CLASS_FILE) { parent = parent.getParent(); } return parent; } /** * Get the offset of the supplied element within the source. * * @param element The element * @return The offset or -1 if it could not be determined. */ public static int getElementOffset(IJavaElement element) throws Exception { IJavaElement parent = getPrimaryElement(element); CompilationUnit cu = null; switch(parent.getElementType()){ case IJavaElement.COMPILATION_UNIT: cu = ASTUtils.getCompilationUnit((ICompilationUnit)parent); break; case IJavaElement.CLASS_FILE: try{ cu = ASTUtils.getCompilationUnit((IClassFile)parent); }catch(IllegalStateException ise){ // no source attachement } break; } if (cu != null) { ASTNode[] nodes = ASTNodeSearchUtil.getDeclarationNodes(element, cu); if (nodes != null && nodes.length > 0){ int offset = nodes[0].getStartPosition(); if (nodes[0] instanceof BodyDeclaration){ Javadoc docs = ((BodyDeclaration)nodes[0]).getJavadoc(); if (docs != null){ offset += docs.getLength() + 1; } } return offset; } } return -1; } /** * Gets the IDocument for the supplied src file. * <p/> * Code borrowed from org.eclipse.jdt.internal.core.JavaModelOperation. * * @param src The src file. * @return The IDocument. */ public static IDocument getDocument(ICompilationUnit src) throws Exception { IBuffer buffer = src.getBuffer(); if(buffer instanceof IDocument){ return (IDocument)buffer; } return new DocumentAdapter(buffer); } /** * Gets the fully qualified name of the supplied java element. * <p/> * NOTE: For easy of determining fields and method segments, they are appended * with a javadoc style '#' instead of the normal '.'. * * @param element The IJavaElement. * * @return The fully qualified name. */ public static String getFullyQualifiedName(IJavaElement element) { IJavaElement parent = element; while(parent.getElementType() != IJavaElement.COMPILATION_UNIT && parent.getElementType() != IJavaElement.CLASS_FILE) { parent = parent.getParent(); } StringBuffer elementName = new StringBuffer() .append(parent.getParent().getElementName()) .append('.') .append(FileUtils.getFileName(parent.getElementName())); switch(element.getElementType()){ case IJavaElement.FIELD: IField field = (IField)element; elementName.append('#').append(field.getElementName()); break; case IJavaElement.METHOD: IMethod method = (IMethod)element; elementName.append('#') .append(method.getElementName()) .append('('); String[] parameters = method.getParameterTypes(); for(int ii = 0; ii < parameters.length; ii++){ if(ii != 0){ elementName.append(", "); } elementName.append( Signature.toString(parameters[ii]).replace('/', '.')); } elementName.append(')'); break; } return elementName.toString(); } /** * Constructs a compilation unit relative name for the supplied type. * <p/> * If the type is imported, in java.lang, or in the same package as the source * file, then the type name returned is unqualified, otherwise the name * returned is the fully qualified type name. * * @param src The compilation unit. * @param type The type. * * @return The relative type name. */ public static String getCompilationUnitRelativeTypeName( ICompilationUnit src, IType type) throws Exception { String typeName = type.getFullyQualifiedName().replace('$', '.'); if(JavaUtils.containsImport(src, type)){ typeName = type.getElementName(); int parentType = type.getParent().getElementType(); if (parentType == IJavaElement.TYPE){ typeName = type.getParent().getElementName() + '.' + typeName; }else if (parentType == IJavaElement.CLASS_FILE){ String parentName = type.getParent().getElementName(); int index = parentName.indexOf('$'); if (index != -1){ parentName = parentName.substring(0, index); typeName = parentName + '.' + typeName; } } }else{ typeName = type.getFullyQualifiedName().replace('$', '.'); } return typeName; } /** * Determines if the supplied src file contains an import for the * supplied type (including wildcard .* imports). * * @param src The compilation unit. * @param type The type. * @return true if the src file has a qualifying import. */ public static boolean containsImport(ICompilationUnit src, String type) throws Exception { return containsImport(src, src.getType(type)); } /** * Determines if the supplied src file contains an import for the * supplied type (including wildcard .* imports). * * @param src The compilation unit. * @param type The type. * @return true if the src file has a qualifying import. */ public static boolean containsImport(ICompilationUnit src, IType type) throws Exception { String typePkg = type.getPackageFragment().getElementName(); IPackageDeclaration[] packages = src.getPackageDeclarations(); String pkg = packages.length > 0 ? packages[0].getElementName() : null; // classes in same package are auto imported. if ((pkg == null && typePkg == null) || (pkg != null && pkg.equals(typePkg))) { return true; } // java.lang is auto imported. if(JAVA_LANG.equals(typePkg)){ return true; } typePkg = typePkg + ".*"; String typeName = type.getFullyQualifiedName().replace('$', '.'); IImportDeclaration[] imports = src.getImports(); for (int ii = 0; ii < imports.length; ii++){ String name = imports[ii].getElementName(); if(name.equals(typeName) || name.equals(typePkg)){ return true; } } return false; } /** * Format a region in the supplied source file. * * @param src The ICompilationUnit. * @param kind The kind of code snippet to format. * @param offset The starting offset of the region to format. * @param length The length of the region to format. */ public static void format(ICompilationUnit src, int kind, int offset, int length) throws Exception { IBuffer buffer = src.getBuffer(); String contents = buffer.getContents(); String delimiter = StubUtility.getLineDelimiterUsed(src); DefaultCodeFormatter formatter = new DefaultCodeFormatter(src.getJavaProject().getOptions(true)); // when the eclipse indent settings differ from vim (tabs vs spaces) then // the inserted method's indent may be a bit off. this is a workaround to // force reformatting of the code from the start of the line to the start of // the next set of code following the new method. Doesn't quite fix indent // formatting of methods in nested classes. while (offset > 0 && !IndentManipulation.isLineDelimiterChar(buffer.getChar(offset - 1))) { offset--; length++; } while ((offset + length) < contents.length() && IndentManipulation.isLineDelimiterChar(buffer.getChar(offset + length))) { length++; } TextEdit edits = formatter.format(kind, contents, offset, length, 0, delimiter); if (edits != null) { int oldLength = contents.length(); Document document = new Document(contents); edits.apply(document); String formatted = document.get(); // jdt formatter can introduce trailing whitespace (javadoc comments), so // we'll remove all trailing whitespace from the formatted section (unless // the user has configured eclim not to do so). length += formatted.length() - oldLength; if (offset < (offset + length)){ String stripTrailingWhitespace = Preferences.getInstance().getValue( src.getJavaProject().getProject(), "org.eclim.java.format.strip_trialing_whitespace"); if ("true".equals(stripTrailingWhitespace)){ String pre = formatted.substring(0, offset); StringBuffer section = new StringBuffer( formatted.substring(offset, offset + length)); StringBuffer post = new StringBuffer( formatted.substring(offset + length)); // account for section not ending at a line delimiter while (!section.toString().endsWith(delimiter) && post.length() > 0){ section.append(post.charAt(0)); post.deleteCharAt(0); } Matcher matcher = TRAILING_WHITESPACE.matcher(section); String stripped = matcher.replaceAll(StringUtils.EMPTY); src.getBuffer().setContents(pre + stripped + post); }else{ src.getBuffer().setContents(formatted); } }else{ src.getBuffer().setContents(formatted); } if (src.isWorkingCopy()) { src.commitWorkingCopy(true, null); } src.save(null, false); } } /** * Gets the java version for which all source is to be compatable with. * * @param project The java project. * @return The source compliance version. */ public static String getCompilerSourceCompliance(IJavaProject project) { return (String)project.getOptions(true).get(JavaCore.COMPILER_SOURCE); } /** * Sets the java version for which all source is to be compatable with. * * @param version The java version. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static void setCompilerSourceCompliance(String version) throws Exception { Map<String, String> options = JavaCore.getOptions(); options.put(JavaCore.COMPILER_SOURCE, version); options.put(JavaCore.COMPILER_COMPLIANCE, version); options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, version); JavaCore.setOptions((Hashtable)options); } /** * Sets the java version for which all source is to be compatable with. * * @param project The java project. * @param version The java version. */ @SuppressWarnings("unchecked") public static void setCompilerSourceCompliance( IJavaProject project, String version) throws Exception { // using project.setOption(String,String) doesn't save the setting. Map<String, String> options = project.getOptions(false); options.put(JavaCore.COMPILER_SOURCE, version); options.put(JavaCore.COMPILER_COMPLIANCE, version); options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, version); project.setOptions(options); } /** * Loads the supplied map to be used in a template with the available * preferences. * * @param project The current project. * @param preferences The eclim preferences. * @param values The values to populate. */ public static void loadPreferencesForTemplate( IProject project, Preferences preferences, Map<String, Object> values) throws Exception { Map<String, String> options = preferences.getValues(project); for(String key : options.keySet()){ String value = options.get(key); values.put(key.replace('.', '_'), value); } } /** * Gets the problems for a given src file. * * @param src The src file. * @return The problems. */ public static IProblem[] getProblems(ICompilationUnit src) throws Exception { return getProblems(src, null); } /** * Gets the problems for a given src file. * * @param src The src file. * @param ids Array of problem ids to accept. * @return The problems. */ public static IProblem[] getProblems(ICompilationUnit src, int[] ids) throws Exception { ICompilationUnit workingCopy = src.getWorkingCopy(null); ProblemRequestor requestor = new ProblemRequestor(ids); try{ workingCopy.discardWorkingCopy(); workingCopy.becomeWorkingCopy(requestor, null); }finally{ workingCopy.discardWorkingCopy(); } List<IProblem> problems = requestor.getProblems(); return (IProblem[])problems.toArray(new IProblem[problems.size()]); } /** * Gets array of IQuickFixProcessor(s). * <p/> * Based on * org.eclipse.jdt.internal.ui.text.correction.JavaCorrectionProcessor#getCorrectionProcessors() * * @param src The src file to get processors for. * @return quick fix processors. */ public static IQuickFixProcessor[] getQuickFixProcessors(ICompilationUnit src) throws Exception { if (correctionProcessors == null) { correctionProcessors = getProcessorDescriptors("quickFixProcessors", true); } IQuickFixProcessor[] processors = new IQuickFixProcessor[correctionProcessors.length]; for(int ii = 0; ii < correctionProcessors.length; ii++){ processors[ii] = (IQuickFixProcessor) correctionProcessors[ii].getProcessor(src, IQuickFixProcessor.class); } return processors; } /** * Gets array of IQuickAssistProcessor(s). * <p/> * Based on * org.eclipse.jdt.internal.ui.text.correction.JavaCorrectionProcessor#getAssistProcessors() * * @param src The src file to get processors for. * @return quick assist processors. */ public static IQuickAssistProcessor[] getQuickAssistProcessors( ICompilationUnit src) throws Exception { if (assistProcessors == null) { assistProcessors = getProcessorDescriptors("quickAssistProcessors", false); } IQuickAssistProcessor[] processors = new IQuickAssistProcessor[assistProcessors.length]; for(int ii = 0; ii < assistProcessors.length; ii++){ processors[ii] = (IQuickAssistProcessor) assistProcessors[ii].getProcessor(src, IQuickAssistProcessor.class); } return processors; } private static ContributedProcessorDescriptor[] getProcessorDescriptors( String id, boolean testMarkerTypes) throws Exception { IConfigurationElement[] elements = Platform.getExtensionRegistry() .getConfigurationElementsFor(JavaUI.ID_PLUGIN, id); ArrayList<ContributedProcessorDescriptor> res = new ArrayList<ContributedProcessorDescriptor>(elements.length); for(int ii = 0; ii < elements.length; ii++){ ContributedProcessorDescriptor desc = new ContributedProcessorDescriptor(elements[ii], testMarkerTypes); IStatus status = desc.checkSyntax(); if(status.isOK()){ res.add(desc); }else{ JavaPlugin.log(status); } } return (ContributedProcessorDescriptor[]) res.toArray(new ContributedProcessorDescriptor[res.size()]); } /** * Gathers problems as a src file is processed. */ public static class ProblemRequestor implements org.eclipse.jdt.core.IProblemRequestor { private ArrayList<IProblem> problems = new ArrayList<IProblem>(); private int[] ids; /** * Constructs a new instance. * * @param ids Array of problem ids to accept. */ public ProblemRequestor(int[] ids) { this.ids = ids; } /** * Gets a list of problems recorded. * * @return The list of problems. */ public List<IProblem> getProblems() { return problems; } /** * {@inheritDoc} */ public void acceptProblem(IProblem problem) { if(ids != null){ for (int ii = 0; ii < ids.length; ii++){ if(problem.getID() == ids[ii]){ problems.add(problem); break; } } }else{ problems.add(problem); } } /** * {@inheritDoc} */ public void beginReporting(){} /** * {@inheritDoc} */ public void endReporting(){} /** * {@inheritDoc} */ public boolean isActive() { return true; } } }