/******************************************************************************* * Copyright (c) 2008, 2016 Spring IDE Developers * 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: * Spring IDE Developers - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.core.java; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IPathVariableManager; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.ElementChangedEvent; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IElementChangedListener; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader; import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException; import org.eclipse.jdt.internal.compiler.env.ClassSignature; import org.eclipse.jdt.internal.compiler.env.EnumConstantSignature; import org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation; import org.eclipse.jdt.internal.compiler.env.IBinaryElementValuePair; import org.eclipse.jdt.internal.compiler.env.IBinaryField; import org.eclipse.jdt.internal.compiler.env.IBinaryMethod; import org.eclipse.jdt.internal.compiler.impl.Constant; import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers; import org.springframework.ide.eclipse.core.SpringCore; import org.springsource.ide.eclipse.commons.core.SpringCoreUtils; /** * Object that caches instances of {@link TypeStructure}. Furthermore this implementation is able to answer if a given * {@link IResource} which represents a class file has structural changes. * <p> * For this implementation a change of class and method level annotation is considered a structural change. * * @author Christian Dupuis * @author Martin Lippert * @since 2.2.0 */ @SuppressWarnings("restriction") public class TypeStructureCache implements ITypeStructureCache { private static final char[][] EMPTY_CHAR_ARRAY = new char[0][]; private IElementChangedListener changedListener = null; /** {@link TypeStructure} instances keyed by full-qualified class names */ private Map<IProject, Map<String, TypeStructure>> typeStructuresByProject = new ConcurrentHashMap<IProject, Map<String, TypeStructure>>(); protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); protected final Lock r = rwl.readLock(); protected final Lock w = rwl.writeLock(); public void startup() { changedListener = new TypeRemovingJavaElementChangeListener(); JavaCore.addElementChangedListener(changedListener); } public void shutdown() { JavaCore.removeElementChangedListener(changedListener); changedListener = null; typeStructuresByProject = null; } /** * Removes {@link TypeStructure}s for a given project. */ public void clearStateForProject(IProject project) { try { w.lock(); typeStructuresByProject.remove(project); } finally { w.unlock(); } } /** * Checks if {@link TypeStructure} instances exist for a given project. */ public boolean hasRecordedTypeStructures(IProject project) { try { r.lock(); return typeStructuresByProject.containsKey(project); } finally { r.unlock(); } } /** * Record {@link TypeStructure} instances of the given <code>resources</code>. */ public void recordTypeStructures(IProject project, IResource... resources) { try { w.lock(); Map<String, TypeStructure> typeStructures = null; if (!typeStructuresByProject.containsKey(project)) { typeStructures = new ConcurrentHashMap<String, TypeStructure>(); typeStructuresByProject.put(project, typeStructures); } else { typeStructures = typeStructuresByProject.get(project); } for (IResource resource : resources) { if (resource.getFileExtension().equals("class") && resource instanceof IFile) { InputStream input = null; try { input = ((IFile) resource).getContents(); ClassFileReader reader = ClassFileReader.read(input, resource.getName()); TypeStructure typeStructure = new TypeStructure(reader); typeStructures.put(new String(reader.getName()).replace('/', '.'), typeStructure); } catch (CoreException e) { } catch (ClassFormatException e) { } catch (IOException e) { } finally { if (input != null) { try { input.close(); } catch (IOException e) { } } } } } } finally { w.unlock(); } } /** * Check if a given {@link IResource} representing a class file has structural changes. */ public boolean hasStructuralChanges(IResource resource, int flags) { try { r.lock(); if (!hasRecordedTypeStructures(resource.getProject())) { return true; } Map<String, TypeStructure> typeStructures = typeStructuresByProject.get(resource.getProject()); if (resource != null && resource.getFileExtension() != null && resource.getFileExtension().equals("java")) { IJavaElement element = JavaCore.create(resource); if (element instanceof ICompilationUnit && ((ICompilationUnit) element).isOpen()) { try { IType[] types = ((ICompilationUnit) element).getAllTypes(); for (IType type : types) { String fqn = type.getFullyQualifiedName(); TypeStructure typeStructure = typeStructures.get(fqn); if (typeStructure == null) { return true; } ClassFileReader reader = getClassFileReaderForClassName(type.getFullyQualifiedName(), resource.getProject()); if (reader != null && hasStructuralChanges(reader, typeStructure, flags)) { return true; } } return false; } catch (JavaModelException e) { SpringCore.log(e); } catch (MalformedURLException e) { SpringCore.log(e); } } } return true; } finally { r.unlock(); } } /** * Removes cached type structures by the given className. */ protected void removeRecordedTyeStructures(IProject project, String className) { try { w.lock(); if (!hasRecordedTypeStructures(project)) { return; } String innerClassName = className + "$"; List<String> typeStructuresToRemove = new ArrayList<String>(); Map<String, TypeStructure> typeStructures = typeStructuresByProject.get(project); for (String recordedClassName : typeStructures.keySet()) { if (className.equals(recordedClassName) || recordedClassName.startsWith(innerClassName)) { typeStructuresToRemove.add(recordedClassName); } } for (String recordedClassName : typeStructuresToRemove) { typeStructures.remove(recordedClassName); } } finally { w.unlock(); } } private static ClassFileReader getClassFileReaderForClassName(String className, IProject project) throws JavaModelException, MalformedURLException { IJavaProject jp = JavaCore.create(project); File outputDirectory = convertPathToFile(project, jp.getOutputLocation()); File classFile = new File(outputDirectory, ClassUtils.getClassFileName(className)); if (classFile.exists() && classFile.canRead()) { try { return ClassFileReader.read(classFile); } catch (ClassFormatException e) { } catch (IOException e) { } } IClasspathEntry[] classpath = jp.getRawClasspath(); for (int i = 0; i < classpath.length; i++) { IClasspathEntry path = classpath[i]; if (path.getEntryKind() == IClasspathEntry.CPE_SOURCE) { outputDirectory = convertPathToFile(project, path.getOutputLocation()); classFile = new File(outputDirectory, ClassUtils.getClassFileName(className)); if (classFile.exists() && classFile.canRead()) { try { return ClassFileReader.read(classFile); } catch (ClassFormatException e) { } catch (IOException e) { } } } } return null; } private static File convertPathToFile(IProject project, IPath path) throws MalformedURLException { if (path != null && project != null && path.removeFirstSegments(1) != null) { IResource resource = project.findMember(path.removeFirstSegments(1)); if (resource != null) { URI uri = resource.getRawLocationURI(); if (uri != null) { String scheme = uri.getScheme(); if (SpringCoreUtils.FILE_SCHEME.equalsIgnoreCase(scheme)) { return toLocalFile(uri); } else { IPathVariableManager variableManager = ResourcesPlugin.getWorkspace().getPathVariableManager(); return toLocalFile(variableManager.resolveURI(uri)); } } } } return null; } private static File toLocalFile(URI locationURI) { if (locationURI == null) return null; try { IFileStore store = EFS.getStore(locationURI); return store.toLocalFile(0, null); } catch (CoreException ex) { SpringCore.log("Error while converting URI to local file: " + locationURI.toString(), ex); } return null; } private boolean hasStructuralChanges(ClassFileReader reader, TypeStructure existingType, int flags) { if (existingType == null) { return true; } // modifiers if (!modifiersEqual(reader.getModifiers(), existingType.modifiers)) { return true; } // generic signature if (!CharOperation.equals(reader.getGenericSignature(), existingType.genericSignature)) { return true; } // superclass name if (!CharOperation.equals(reader.getSuperclassName(), existingType.superclassName)) { return true; } // class level annotations if ((flags & FLAG_ANNOTATION) != 0) { IBinaryAnnotation[] existingAnnotations = existingType.getAnnotations(); IBinaryAnnotation[] newAnnotations = reader.getAnnotations(); if (!annotationsEqual(existingAnnotations, newAnnotations, flags)) { return true; } } // tag bits; standard annotations like @Deprecated if (reader.getTagBits() != existingType.getTagBits()) { return true; } // interfaces char[][] existingIfs = existingType.interfaces; char[][] newIfsAsChars = reader.getInterfaceNames(); if (newIfsAsChars == null) { newIfsAsChars = EMPTY_CHAR_ARRAY; } // damn I'm lazy... if (existingIfs == null) { existingIfs = EMPTY_CHAR_ARRAY; } if (existingIfs.length != newIfsAsChars.length) return true; new_interface_loop: for (int i = 0; i < newIfsAsChars.length; i++) { for (int j = 0; j < existingIfs.length; j++) { if (CharOperation.equals(existingIfs[j], newIfsAsChars[i])) { continue new_interface_loop; } } return true; } // fields IBinaryField[] newFields = reader.getFields(); if (newFields == null) { newFields = TypeStructure.NoField; } IBinaryField[] existingFs = existingType.binFields; if (newFields.length != existingFs.length) return true; new_field_loop: for (int i = 0; i < newFields.length; i++) { IBinaryField field = newFields[i]; char[] fieldName = field.getName(); for (int j = 0; j < existingFs.length; j++) { if (CharOperation.equals(existingFs[j].getName(), fieldName)) { if (!modifiersEqual(field.getModifiers(), existingFs[j].getModifiers())) { return true; } if (!CharOperation.equals(existingFs[j].getTypeName(), field.getTypeName())) { return true; } if ((flags & FLAG_ANNOTATION) != 0) { if (!annotationsEqual(field.getAnnotations(), existingFs[j].getAnnotations(), flags)) { return true; } } continue new_field_loop; } } return true; } // methods IBinaryMethod[] newMethods = reader.getMethods(); if (newMethods == null) { newMethods = TypeStructure.NoMethod; } char[] fileName = reader.getFileName(); IBinaryMethod[] existingMs = existingType.binMethods; if (newMethods.length != existingMs.length) return true; new_method_loop: for (int i = 0; i < newMethods.length; i++) { IBinaryMethod method = newMethods[i]; char[] methodName = method.getSelector(); for (int j = 0; j < existingMs.length; j++) { if (CharOperation.equals(existingMs[j].getSelector(), methodName)) { // candidate match if (!CharOperation.equals(method.getMethodDescriptor(), existingMs[j].getMethodDescriptor())) { continue; // might be overloading } else { // matching sigs if (!modifiersEqual(method.getModifiers(), existingMs[j].getModifiers())) { return true; } if ((flags & FLAG_ANNOTATION) != 0) { if (!annotationsEqual(method.getAnnotations(), existingMs[j].getAnnotations(), flags)) { return true; } if (!parameterAnnotationsEquals(method, existingMs[j], fileName, flags)) { return true; } } continue new_method_loop; } } } return true; // (no match found) } return false; } private static boolean parameterAnnotationsEquals(IBinaryMethod newMethod, IBinaryMethod existingMethod, char[] fileName, int flags) { char[][] argumentNames = newMethod.getArgumentNames(); char[][] existingArgumentNames = existingMethod.getArgumentNames(); if (argumentNames == null && existingArgumentNames == null) return true; int argumentCount = argumentNames != null ? argumentNames.length : 0; int existingArgumentCount = existingArgumentNames != null ? existingArgumentNames.length : 0; if (argumentCount != existingArgumentCount) return false; for (int i = 0; i < argumentCount; i++) { IBinaryAnnotation[] parameterAnnotations = getParameterAnnotation(newMethod, i, fileName); IBinaryAnnotation[] existingParameterAnnotations = getParameterAnnotation(existingMethod, i, fileName); if (!annotationsEqual(parameterAnnotations, existingParameterAnnotations, flags)) { return false; } } return true; } // changed API of IBinaryMethod (between Eclipse 4.5 and Eclipse 4.6) // therefore adapting to this via reflection to use the correct existing method private static IBinaryAnnotation[] getParameterAnnotation(IBinaryMethod newMethod, int i, char[] fileName) { IBinaryAnnotation[] result = null; // try the old method first try { try { Method getParameterAnnotationsMethod = newMethod.getClass().getMethod("getParameterAnnotations", int.class); if (getParameterAnnotationsMethod != null) { getParameterAnnotationsMethod.setAccessible(true); result = (IBinaryAnnotation[]) getParameterAnnotationsMethod.invoke(newMethod, i); } } catch (NoSuchMethodException e) { // if the old method is not there, try the new one Method getParameterAnnotationsMethod = newMethod.getClass().getMethod("getParameterAnnotations", int.class, char[].class); if (getParameterAnnotationsMethod != null) { getParameterAnnotationsMethod.setAccessible(true); result = (IBinaryAnnotation[]) getParameterAnnotationsMethod.invoke(newMethod, i, fileName); } } } catch (Exception e) { SpringCore.log(e); } return result; } private static boolean annotationsEqual(IBinaryAnnotation[] existingAnnotations, IBinaryAnnotation[] newAnnotations, int flags) { if (existingAnnotations == null) { existingAnnotations = TypeStructure.NoAnnotation; } if (newAnnotations == null) { newAnnotations = TypeStructure.NoAnnotation; } if (existingAnnotations.length != newAnnotations.length) { return false; } new_annotation_loop: for (int i = 0; i < newAnnotations.length; i++) { for (int j = 0; j < existingAnnotations.length; j++) { if (CharOperation.equals(newAnnotations[j].getTypeName(), existingAnnotations[i].getTypeName())) { // compare annotation parameters if ((flags & FLAG_ANNOTATION_VALUE) != 0) { IBinaryElementValuePair[] newParameters = newAnnotations[j].getElementValuePairs(); IBinaryElementValuePair[] existingParameters = existingAnnotations[j].getElementValuePairs(); if (newParameters == null) { newParameters = TypeStructure.NoElement; } if (existingParameters == null) { existingParameters = TypeStructure.NoElement; } if (existingParameters.length != newParameters.length) { return false; } for (int k = 0; k < newParameters.length; k++) { for (int l = 0; l < existingParameters.length; l++) { char[] newName = newParameters[l].getName(); char[] existingName = existingParameters[l].getName(); Object newValue = newParameters[l].getValue(); Object existingValue = existingParameters[l].getValue(); if (!CharOperation.equals(newName, existingName)) { return false; } if (!parameterValuesEquals(flags, newValue, existingValue)) { return false; } } } } continue new_annotation_loop; } } return false; } return true; } private static boolean parameterValuesEquals(int flags, Object newValue, Object existingValue) { if (newValue.getClass().isArray() && existingValue.getClass().isArray()) { Object[] newValueArray = (Object[]) newValue; Object[] existingValueArray = (Object[]) existingValue; if (newValueArray.length != existingValueArray.length) { return false; } for (int i = 0; i < newValueArray.length; i++) { if (!parameterValuesEquals(flags, newValueArray[i], existingValueArray[i])) { return false; } } } else if (newValue instanceof ClassSignature) { if (existingValue instanceof ClassSignature) { if (!CharOperation.equals(((ClassSignature) newValue).getTypeName(), ((ClassSignature) existingValue).getTypeName())) { return false; } } else { return false; } } else if (newValue instanceof Constant) { if (existingValue instanceof Constant) { if (!((Constant) newValue).hasSameValue((Constant) existingValue)) { return false; } } else { return false; } } else if (newValue instanceof EnumConstantSignature) { if (existingValue instanceof EnumConstantSignature) { if (!(CharOperation.equals(((EnumConstantSignature) newValue).getTypeName(), ((EnumConstantSignature) existingValue).getTypeName()) && CharOperation.equals( ((EnumConstantSignature) newValue).getEnumConstantName(), ((EnumConstantSignature) existingValue).getEnumConstantName()))) { return false; } } else { return false; } } else if (newValue instanceof IBinaryAnnotation) { if (existingValue instanceof EnumConstantSignature) { if (!annotationsEqual(new IBinaryAnnotation[] { (IBinaryAnnotation) newValue }, new IBinaryAnnotation[] { (IBinaryAnnotation) existingValue }, flags)) { return false; } } else { return false; } } return true; } private static boolean modifiersEqual(int eclipseModifiers, int resolvedTypeModifiers) { resolvedTypeModifiers = resolvedTypeModifiers & ExtraCompilerModifiers.AccJustFlag; eclipseModifiers = eclipseModifiers & ExtraCompilerModifiers.AccJustFlag; return (eclipseModifiers == resolvedTypeModifiers); } private class TypeRemovingJavaElementChangeListener implements IElementChangedListener { public void elementChanged(ElementChangedEvent event) { if (event.getType() == ElementChangedEvent.POST_CHANGE) { Object obj = event.getSource(); if (obj instanceof IJavaElementDelta) { IJavaElementDelta delta = (IJavaElementDelta) obj; iterateChildren(new IJavaElementDelta[] { delta }, new IJavaProject[1]); } } } private void iterateChildren(IJavaElementDelta[] deltas, IJavaProject[] javaProject) { for (IJavaElementDelta delta : deltas) { if (delta.getElement() instanceof IJavaProject) { javaProject[0] = (IJavaProject) delta.getElement(); } // process removed element IJavaElementDelta[] removedDeltas = delta.getRemovedChildren(); for (IJavaElementDelta removedDelta : removedDeltas) { IJavaElement je = removedDelta.getElement(); if (je instanceof ICompilationUnit) { StringBuilder sb = new StringBuilder(); guessClassName(je, sb); if (javaProject[0] != null) { removeRecordedTyeStructures(javaProject[0].getProject(), sb.toString()); } } } iterateChildren(delta.getAffectedChildren(), javaProject); } } private void guessClassName(IJavaElement cu, StringBuilder sb) { if (cu instanceof IPackageFragment) { if (cu.getElementName().length() > 0) { sb.insert(0, cu.getElementName() + "."); } } else if (cu != null) { if (cu instanceof ICompilationUnit) { int ix = cu.getElementName().lastIndexOf('.'); String name = cu.getElementName().substring(0, ix); sb.insert(0, name); } else { sb.insert(0, cu.getElementName()); } guessClassName(cu.getParent(), sb); } } } }