/** * Copyright (c) 2010, 2012 Darmstadt University of Technology. * 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: * Marcel Bruch - initial API and implementation. * Kevin Munk - Extension of method for finding package names and correct regular expression for Java identifier. * Extension for package creation and helper methods. */ package org.eclipse.recommenders.testing.jdt; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static java.util.Arrays.asList; import static org.eclipse.recommenders.testing.jdt.AstUtils.*; import static org.eclipse.recommenders.utils.Checks.*; import static org.eclipse.recommenders.utils.Pair.newPair; import static org.eclipse.recommenders.utils.Throws.throwUnhandledException; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.ArrayUtils; 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.IResourceVisitor; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.recommenders.utils.Pair; public class JavaProjectFixture { /** * A regular expression group that can be used to match Java identifier. Java identifier can not start with a digit, * but can contain underscore and dollar signs. Java identifiers can contain ASCII or Unicode letters and digits. */ public static final String JAVA_IDENTIFIER_REGEX = "([a-zA-Z_$\\p{Lu}\\p{Ll}]{1}" + "[a-zA-Z_$0-9\\p{Lu}\\p{Ll}\\p{Nl}]*)"; /** * A null reference to make code more readable. */ public static final IProgressMonitor NULL_PROGRESS_MONITOR = null; public static String findClassName(final CharSequence source) { Pattern p = Pattern.compile(".*?class\\s+" + JAVA_IDENTIFIER_REGEX + ".*", Pattern.DOTALL); Matcher matcher = p.matcher(source); if (!matcher.matches()) { p = Pattern.compile(".*interface\\s+" + JAVA_IDENTIFIER_REGEX + ".*", Pattern.DOTALL); matcher = p.matcher(source); } if (!matcher.matches()) { p = Pattern.compile(".*enum\\s+" + JAVA_IDENTIFIER_REGEX + ".*", Pattern.DOTALL); matcher = p.matcher(source); } assertTrue(matcher.matches()); return matcher.group(1); } public static List<String> findInnerClassNames(final CharSequence source) { String declaringType = findClassName(source); List<String> names = newArrayList(); Pattern p = Pattern.compile("(class|interface|enum)\\s+" + JAVA_IDENTIFIER_REGEX, Pattern.DOTALL); Matcher matcher = p.matcher(source); while (matcher.find()) { final String name = matcher.group(2); if (!name.equals(declaringType)) { names.add(declaringType + "$" + name); } } return names; } public static List<String> findAnonymousClassNames(final CharSequence source) { String declaringType = findClassName(source); int num = 1; List<String> names = newArrayList(); // new <name> ( ... ) { Pattern p = Pattern.compile("new\\s*?" + JAVA_IDENTIFIER_REGEX + "\\s*?\\([^)]*?\\)\\s*?\\{", Pattern.DOTALL); Matcher matcher = p.matcher(source); while (matcher.find()) { final String name = matcher.group(1); if (!name.equals(declaringType)) { names.add(declaringType + "$" + num++); } } return names; } /** * Finds the package name from the package declaration inside the source code. * * @param source * the source code * @return the package name or "" if no package declaration was found */ public static String findPackageName(final CharSequence source) { Pattern p = Pattern.compile( ".*" // any characters at the beginning + "package\\s+" // package declaration + "(" // beginning of the package name group + JAVA_IDENTIFIER_REGEX // the first part of the package + "{1}" // must occur one time + "([.]{1}" // the following parts of the package must begin with a dot + JAVA_IDENTIFIER_REGEX // followed by a java identifier + ")*" // the (.identifier) group can occur multiple times or not at all + ")" // closing of the package name group + "[;]+.*", // the following ; and the rest of the source code Pattern.DOTALL); Matcher matcher = p.matcher(source); if (matcher.matches()) { return matcher.group(1); } return ""; } private IJavaProject javaProject; private ASTParser parser; public JavaProjectFixture(final IWorkspace workspace, final String projectName) { createJavaProject(workspace, projectName); createParser(); } private void createJavaProject(final IWorkspace workspace, final String projectName) { final IProject project = workspace.getRoot().getProject(projectName); final IWorkspaceRunnable populate = new IWorkspaceRunnable() { @Override public void run(final IProgressMonitor monitor) throws CoreException { createAndOpenProject(project); if (!hasJavaNature(project)) { addJavaNature(project); configureProjectClasspath(); } } private void createAndOpenProject(final IProject project) throws CoreException { if (!project.exists()) { project.create(NULL_PROGRESS_MONITOR); } project.open(NULL_PROGRESS_MONITOR); } private boolean hasJavaNature(final IProject project) throws CoreException { final IProjectDescription description = project.getDescription(); final String[] natures = description.getNatureIds(); return ArrayUtils.contains(natures, JavaCore.NATURE_ID); } private void configureProjectClasspath() throws JavaModelException { final Set<IClasspathEntry> entries = newHashSet(); final IClasspathEntry[] rawClasspath = javaProject.getRawClasspath(); final IClasspathEntry defaultJREContainerEntry = JavaRuntime.getDefaultJREContainerEntry(); entries.addAll(asList(rawClasspath)); entries.add(defaultJREContainerEntry); final IClasspathEntry[] entriesArray = entries.toArray(new IClasspathEntry[entries.size()]); javaProject.setRawClasspath(entriesArray, NULL_PROGRESS_MONITOR); } private void addJavaNature(final IProject project) throws CoreException { final IProjectDescription description = project.getDescription(); final String[] natures = description.getNatureIds(); final String[] newNatures = ArrayUtils.add(natures, JavaCore.NATURE_ID); description.setNatureIds(newNatures); project.setDescription(description, NULL_PROGRESS_MONITOR); javaProject = JavaCore.create(project); } }; try { workspace.run(populate, NULL_PROGRESS_MONITOR); } catch (final Exception e) { throwUnhandledException(e); } javaProject = JavaCore.create(project); } private void createParser() { parser = ASTParser.newParser(AST.JLS3); // parser.setEnvironment(...) enables bindings resolving parser.setProject(javaProject); // enables bindings and IJavaElement // resolving parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setResolveBindings(true); } public Pair<CompilationUnit, List<Integer>> parseWithMarkers(final String content) { final Pair<String, List<Integer>> contentMarkersPair = findMarkers(content); final String contentWoMarkers = contentMarkersPair.getFirst(); final List<Integer> markers = contentMarkersPair.getSecond(); final CompilationUnit cu = parse(contentWoMarkers); return newPair(cu, markers); } public Pair<String, List<Integer>> findMarkers(final CharSequence content) { return findMarkers(content, MARKER); } public Pair<String, List<Integer>> findMarkers(final CharSequence content, String marker) { final List<Integer> markers = new ArrayList<>(); int pos = 0; final StringBuilder sb = new StringBuilder(content); while ((pos = sb.indexOf(marker, pos)) != -1) { sb.deleteCharAt(pos); markers.add(pos); ensureIsTrue(pos <= sb.length()); pos--; } return newPair(sb.toString(), markers); } public CompilationUnit parse(final String content) { parser.setSource(content.toCharArray()); parser.setUnitName(findClassName(content) + ".java"); return cast(parser.createAST(NULL_PROGRESS_MONITOR)); } /** * Creates the file with the content in the default package folder. The markers in the content will be removed * beforehand. The package specified in the content will not be created. After creation of the file the project will * be refreshed and built. * * @param contentWithMarkers * the code with markers(see {@link AstUtils}.MARKER) * @return the Pair of the ICompilationUnit and the List of marker positions in the code provided * @throws CoreException */ public Pair<ICompilationUnit, List<Integer>> createFileAndParseWithMarkers(final CharSequence contentWithMarkers) throws CoreException { return createFileAndParseWithMarkers(contentWithMarkers, MARKER); } public Pair<ICompilationUnit, List<Integer>> createFileAndParseWithMarkers(final CharSequence contentWithMarkers, String marker) throws CoreException { final Pair<String, List<Integer>> content = findMarkers(contentWithMarkers, marker); final ICompilationUnit cu = createFile(content.getFirst(), false); refreshAndBuildProject(); return Pair.newPair(cu, content.getSecond()); } /** * Creates the package folders and the file with the content inside of this package. The markers in the content will * be removed beforehand. After creation of the file the project will be refreshed and built. * * @param contentWithMarkers * the code with markers(see {@link AstUtils}.MARKER) * @return the Pair of the ICompilationUnit and the List of marker positions in the code provided * @throws CoreException */ public Pair<ICompilationUnit, List<Integer>> createFileAndPackageAndParseWithMarkers( final CharSequence contentWithMarkers) throws CoreException { final Pair<String, List<Integer>> content = findMarkers(contentWithMarkers); createPackage(content.getFirst()); final ICompilationUnit cu = createFile(content.getFirst(), true); refreshAndBuildProject(); return Pair.newPair(cu, content.getSecond()); } /** * Refreshes the resources of this project and initiates a full build. * * @throws CoreException */ public void refreshAndBuildProject() throws CoreException { final IProject project = javaProject.getProject(); project.refreshLocal(IResource.DEPTH_INFINITE, NULL_PROGRESS_MONITOR); project.build(IncrementalProjectBuilder.FULL_BUILD, NULL_PROGRESS_MONITOR); } /** * Creates the folders that represent the package/s defined in the source string. If the package name was not found, * no folders will be created. If some or all of the folders exist, these will not be overwritten. After the * creation of the folders, the internal java project will be refreshed. * * @param content * the content of the file which package declaration will be used to create the package/s. * @throws CoreException */ public void createPackage(String content) throws CoreException { // get package from the code String packageName = findPackageName(content); if (!packageName.equalsIgnoreCase("")) { final IProject project = javaProject.getProject(); // append project and package folders IPath projectPath = project.getLocation().addTrailingSeparator(); String relativeFilePath = packageName.replace('.', IPath.SEPARATOR); relativeFilePath += String.valueOf(IPath.SEPARATOR); // create package folders IPath packagePath = new Path(projectPath.toString() + relativeFilePath); File packageDirectory = packagePath.toFile(); packageDirectory.mkdirs(); // refresh to prevent that the file creation fails project.refreshLocal(IResource.DEPTH_INFINITE, NULL_PROGRESS_MONITOR); } } /** * Creates the compilation unit with the class name found in the content. If the content has a package declaration * the class will be put inside of this package. For this the package must be exist. The project will not be * refreshed and built after creation of this file.<br> * <br> * To create a file that has markers in it, use the method createFileAndParseWithMarkers() or * createFileAndPackageAndParseWithMarkers(). * * @see createPackage(String) * @see refreshAndBuildProject() * @param content * the content of the compilation unit. Must be java source code with or without package declaration but * with a java class definition * @param usePackage * if the package as declared in the content will be used to create the file. Means, if a package * declaration exists in the content this file will be created inside of this package, otherwise the * default package will be used. * @return the created compilation compilation unit * @throws CoreException */ public ICompilationUnit createFile(final String content, boolean usePackage) throws CoreException { final IProject project = javaProject.getProject(); // get filename final String fileName = findClassName(content) + ".java"; StringBuilder relativeFilePath = new StringBuilder(); if (usePackage) { // get package from the code String packageName = findPackageName(content); if (!packageName.equalsIgnoreCase("")) { relativeFilePath.append(packageName.replace('.', IPath.SEPARATOR)); relativeFilePath.append(String.valueOf(IPath.SEPARATOR)); } } // add the file name and get the file relativeFilePath.append(fileName); final IPath path = new Path(relativeFilePath.toString()); final IFile file = project.getFile(path); // delete file if (file.exists()) { file.delete(true, NULL_PROGRESS_MONITOR); } // create file final ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes()); file.create(is, true, NULL_PROGRESS_MONITOR); int attempts = 0; while (!file.exists()) { try { Thread.sleep(100); } catch (InterruptedException e) { // Do nothing } attempts++; if (attempts > 10) { throw new IllegalStateException("Failed to create file"); } } ICompilationUnit cu = (ICompilationUnit) javaProject.findElement(path); while (cu == null) { cu = (ICompilationUnit) javaProject.findElement(path); } return cu; } /** * Goes through the project and deletes all Java and Class files. * * @throws CoreException */ public void clear() throws CoreException { final IProject project = javaProject.getProject(); project.accept(new IResourceVisitor() { @Override public boolean visit(final IResource resource) throws CoreException { switch (resource.getType()) { case IResource.FILE: if (resource.getName().endsWith(".class") || resource.getName().endsWith(".java")) { resource.delete(true, NULL_PROGRESS_MONITOR); } } return true; } }); } /** * Deletes the project inclusive content from the disk. <b>Warning:</b> This Fixture is no longer usable after doing * this. * * @throws CoreException */ public void deleteProject() throws CoreException { javaProject.getProject().delete(true, true, NULL_PROGRESS_MONITOR); } /** * Retrieves the inner java project managed by this fixture. * * @return the inner java project managed by this fixture */ public IJavaProject getJavaProject() { return javaProject; } /** * Removes all markers from the content. * * @param content * where the markers will be removed * @return the content without any markers */ public String removeMarkers(final String content) { return content.replaceAll(MARKER_ESCAPE, ""); } }