| /******************************************************************************* |
| * Copyright (c) 2015, 2016 GK Software AG. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Stephan Herrmann - initial API and implementation |
| *******************************************************************************/ |
| package org.aspectj.org.eclipse.jdt.core.util; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.UnsupportedEncodingException; |
| |
| import org.eclipse.core.resources.IContainer; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| 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.Status; |
| import org.aspectj.org.eclipse.jdt.core.IClasspathEntry; |
| import org.aspectj.org.eclipse.jdt.core.IJavaElement; |
| import org.aspectj.org.eclipse.jdt.core.IJavaProject; |
| import org.aspectj.org.eclipse.jdt.core.IPackageFragmentRoot; |
| import org.aspectj.org.eclipse.jdt.core.IType; |
| import org.aspectj.org.eclipse.jdt.core.JavaCore; |
| import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation; |
| import org.aspectj.org.eclipse.jdt.core.dom.IMethodBinding; |
| import org.aspectj.org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ExternalAnnotationProvider; |
| import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper; |
| import org.aspectj.org.eclipse.jdt.internal.core.ClasspathEntry; |
| import org.aspectj.org.eclipse.jdt.internal.core.util.KeyToSignature; |
| |
| /** |
| * Utilities for accessing and manipulating text files that externally define annotations for a given Java type. |
| * Files are assumed to be in ".eea format", a textual representation of annotated signatures of members of a given type. |
| * |
| * @since 3.11 |
| * @noinstantiate This class is not intended to be instantiated by clients. |
| */ |
| public final class ExternalAnnotationUtil { |
| |
| /** Representation of a 'nullable' annotation, independent of the concrete annotation name used in Java sources. */ |
| public static final char NULLABLE = ExternalAnnotationProvider.NULLABLE; |
| |
| /** Representation of a 'nonnull' annotation, independent of the concrete annotation name used in Java sources. */ |
| public static final char NONNULL = ExternalAnnotationProvider.NONNULL; |
| |
| /** |
| * Represents absence of a null annotation. Useful for removing an existing null annotation. |
| * This character is used only internally, it is not part of the Eclipse External Annotation file format. |
| */ |
| public static final char NO_ANNOTATION = ExternalAnnotationProvider.NO_ANNOTATION; |
| |
| /** Strategy for merging a new signature with an existing (possibly annotated) signature. */ |
| public static enum MergeStrategy { |
| /** Unconditionally replace the signature. */ |
| REPLACE_SIGNATURE, |
| /** Override existing annotations, keeping old annotations in locations that are not annotated in the new signature. */ |
| OVERWRITE_ANNOTATIONS, |
| /** Only add new annotations, never remove or overwrite existing annotations. */ |
| ADD_ANNOTATIONS |
| } |
| |
| private static final int POSITION_RETURN_TYPE = -1; |
| private static final int POSITION_FULL_SIGNATURE = -2; |
| |
| /** |
| * Answer the give method's signature in class file format. |
| * @param methodBinding binding representing a method |
| * @return a signature in class file format |
| */ |
| public static String extractGenericSignature(IMethodBinding methodBinding) { |
| // Note that IMethodBinding.binding is not accessible, hence we need to recover the signature from the key: |
| KeyToSignature parser = new KeyToSignature(methodBinding.getKey(), KeyToSignature.SIGNATURE, true); |
| parser.parse(); |
| return parser.toString(); |
| } |
| |
| /** |
| * Answer the given types's signature in class file format. |
| * @param type binding representing a type |
| * @return a signature in class file format |
| */ |
| public static String extractGenericTypeSignature(ITypeBinding type) { |
| KeyToSignature parser = new KeyToSignature(type.getKey(), KeyToSignature.SIGNATURE, true); |
| parser.parse(); |
| return parser.toString(); |
| } |
| |
| /** |
| * Insert an encoded annotation into the given methodSignature affecting its return type. |
| * <p> |
| * This method is suitable for declaration annotations. |
| * </p> |
| * @param methodSignature a method signature in class file format |
| * @param annotation one of {@link #NULLABLE} and {@link #NONNULL}. |
| * @param mergeStrategy when passing {@link MergeStrategy#ADD_ANNOTATIONS} this method will |
| * refuse to overwrite any existing annotation in the specified location |
| * @return the modified method signature, or the original signature if modification would |
| * conflict with the given merge strategy. |
| * @throws IllegalArgumentException if the method signature is malformed or its return type is not a reference type. |
| */ |
| public static String insertReturnAnnotation(String methodSignature, char annotation, MergeStrategy mergeStrategy) { |
| int close = methodSignature.indexOf(')'); |
| if (close == -1 || close > methodSignature.length()-4) |
| throw new IllegalArgumentException("Malformed method signature"); //$NON-NLS-1$ |
| switch (methodSignature.charAt(close+1)) { |
| case 'L': case 'T': case '[': |
| return insertAt(methodSignature, close+2, annotation, mergeStrategy); |
| } |
| throw new IllegalArgumentException("Return type is not a reference type"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Insert an encoded annotation into the given methodSignature affecting one of its parameters. |
| * <p> |
| * This method is suitable for declaration annotations. |
| * </p> |
| * @param methodSignature a method signature in class file format |
| * @param paramIdx 0-based index of the parameter to which the annotation should be attached |
| * @param annotation one of {@link #NULLABLE} and {@link #NONNULL}. |
| * @param mergeStrategy when passing {@link MergeStrategy#ADD_ANNOTATIONS} this method will |
| * refuse to overwrite any existing annotation in the specified location |
| * @return the modified method signature, or the original signature if modification would |
| * conflict with the given merge strategy. |
| * @throws IllegalArgumentException if the method signature is malformed or its specified parameter type is not a reference type. |
| */ |
| public static String insertParameterAnnotation(String methodSignature, int paramIdx, char annotation, MergeStrategy mergeStrategy) |
| { |
| SignatureWrapper wrapper = new SignatureWrapper(methodSignature.toCharArray()); |
| wrapper.start = 1; |
| for (int i = 0; i < paramIdx; i++) |
| wrapper.start = wrapper.computeEnd() + 1; |
| int start = wrapper.start; |
| switch (methodSignature.charAt(start)) { |
| case 'L': case 'T': case '[': |
| return insertAt(methodSignature, start+1, annotation, mergeStrategy); |
| } |
| throw new IllegalArgumentException("Paramter type is not a reference type"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Answer the external annotation file corresponding to the given type as seen from the given project. |
| * Note that manipulation of external annotations is only supported for annotation files in the workspace, |
| * and only in directory layout, not from zip files. |
| * @param project current project that references the given type from a jar file. |
| * @param type the type for which external annotations are sought |
| * @param monitor progress monitor to be passed through into file operations |
| * @return a file assumed (but not checked) to be in .eea format. The file may not "exist". |
| * Can be null if the given type is not contained in a jar file for which an external annotation path |
| * has been defined in the context of the given project. |
| * @throws CoreException Signals a problem in accessing any of the relevant elements: the project, the type, |
| * the containing jar file and finally the sought annotation file. |
| */ |
| public static IFile getAnnotationFile(IJavaProject project, ITypeBinding type, IProgressMonitor monitor) throws CoreException { |
| |
| IType targetType = project.findType(type.getErasure().getQualifiedName()); |
| if (!targetType.exists()) |
| return null; |
| |
| String binaryTypeName = targetType.getFullyQualifiedName('$').replace('.', '/'); |
| |
| IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) targetType.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); |
| IClasspathEntry entry = packageRoot.getResolvedClasspathEntry(); |
| IPath annotationPath = ClasspathEntry.getExternalAnnotationPath(entry, project.getProject(), false); |
| |
| if (annotationPath == null) |
| return null; |
| |
| IWorkspaceRoot workspaceRoot = project.getProject().getWorkspace().getRoot(); |
| |
| if (annotationPath.segmentCount() > 1) { |
| IFile annotationZip = workspaceRoot.getFile(annotationPath); |
| if (annotationZip.exists()) |
| return null; |
| } |
| |
| annotationPath = annotationPath.append(binaryTypeName).addFileExtension(ExternalAnnotationProvider.ANNOTATION_FILE_EXTENSION); |
| return workspaceRoot.getFile(annotationPath); |
| } |
| |
| /** |
| * Update the given external annotation file with details regarding annotations of one specific method or field. |
| * If the specified member already has external annotations, old and new annotations will be merged, |
| * with priorities controlled by the parameter 'mergeStrategy'. |
| * <p> |
| * This method is suitable for declaration annotations and type use annotations. |
| * </p> |
| * @param typeName binary name (slash separated) of the type being annotated |
| * @param file a file assumed to be in .eea format, will be created if it doesn't exist. |
| * @param selector selector of the method or field |
| * @param originalSignature unannotated signature of the member, used for identification |
| * @param annotatedSignature new signatures whose annotations should be superimposed on the member |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired |
| * @throws CoreException if access to the file fails |
| * @throws IOException if reading file content fails |
| */ |
| public static void annotateMember(String typeName, IFile file, String selector, String originalSignature, String annotatedSignature, |
| MergeStrategy mergeStrategy, IProgressMonitor monitor) |
| throws CoreException, IOException |
| { |
| annotateMember(typeName, file, selector, originalSignature, annotatedSignature, POSITION_FULL_SIGNATURE, mergeStrategy, monitor); |
| } |
| |
| /** |
| * Update the given external annotation file with details regarding annotations of the return type of a given method. |
| * If the specified method already has external annotations, old and new annotations will be merged, |
| * with priorities controlled by the parameter 'mergeStrategy'. |
| * <p> |
| * This method is suitable for declaration annotations and type use annotations. |
| * </p> |
| * @param typeName binary name (slash separated) of the type being annotated |
| * @param file a file assumed to be in .eea format, will be created if it doesn't exist. |
| * @param selector selector of the method |
| * @param originalSignature unannotated signature of the member, used for identification |
| * @param annotatedReturnType signature of the new return type whose annotations should be superimposed on the method |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired |
| * @throws CoreException if access to the file fails |
| * @throws IOException if reading file content fails |
| * @throws IllegalArgumentException if the annotatedReturnType does not structurally match to originalSignature |
| */ |
| public static void annotateMethodReturnType(String typeName, IFile file, String selector, String originalSignature, |
| String annotatedReturnType, MergeStrategy mergeStrategy, IProgressMonitor monitor) |
| throws CoreException, IOException, IllegalArgumentException |
| { |
| annotateMember(typeName, file, selector, originalSignature, annotatedReturnType, POSITION_RETURN_TYPE, mergeStrategy, monitor); |
| } |
| |
| /** |
| * Update the given external annotation file with details regarding annotations of a parameter type of a given method. |
| * If the specified method already has external annotations, old and new annotations will be merged, |
| * with priorities controlled by the parameter 'mergeStrategy'. |
| * <p> |
| * This method is suitable for declaration annotations and type use annotations. |
| * </p> |
| * @param typeName binary name (slash separated) of the type being annotated |
| * @param file a file assumed to be in .eea format, will be created if it doesn't exist. |
| * @param selector selector of the method |
| * @param originalSignature unannotated signature of the member, used for identification |
| * @param annotatedParameterType signature of the new parameter type whose annotations should be superimposed on the method |
| * @param paramIdx 0-based index of the parameter to which the annotation should be attached |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired |
| * @throws CoreException if access to the file fails |
| * @throws IOException if reading file content fails |
| * @throws IllegalArgumentException if the annotatedParameterType does not structurally match to originalSignature |
| */ |
| public static void annotateMethodParameterType(String typeName, IFile file, String selector, String originalSignature, |
| String annotatedParameterType, int paramIdx, MergeStrategy mergeStrategy, IProgressMonitor monitor) |
| throws CoreException, IOException, IllegalArgumentException |
| { |
| annotateMember(typeName, file, selector, originalSignature, annotatedParameterType, paramIdx, mergeStrategy, monitor); |
| } |
| |
| private static void annotateMember(String typeName, IFile file, String selector, String originalSignature, String annotatedSignature, |
| int updatePosition, MergeStrategy mergeStrategy, IProgressMonitor monitor) |
| throws CoreException, IOException, IllegalArgumentException |
| { |
| |
| if (!file.exists()) { |
| // assemble full annotatedSignature (don't bother merging since no previous signature exists): |
| annotatedSignature = updateSignature(originalSignature, annotatedSignature, updatePosition, MergeStrategy.REPLACE_SIGNATURE); |
| |
| StringBuffer newContent= new StringBuffer(); |
| // header: |
| newContent.append(ExternalAnnotationProvider.CLASS_PREFIX); |
| newContent.append(typeName).append('\n'); |
| // new entry: |
| newContent.append(selector).append('\n'); |
| newContent.append(' ').append(originalSignature).append('\n'); |
| newContent.append(' ').append(annotatedSignature).append('\n'); |
| |
| createNewFile(file, newContent.toString(), monitor); |
| } else { |
| BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents())); |
| StringBuffer newContent = new StringBuffer(); |
| try { |
| newContent.append(reader.readLine()).append('\n'); // skip class name |
| String line; |
| while ((line = reader.readLine()) != null) { |
| if (line.isEmpty()) { |
| newContent.append('\n'); |
| continue; |
| } |
| if (!Character.isJavaIdentifierStart(line.charAt(0)) && line.charAt(0) != '<') { |
| newContent.append(line).append('\n'); |
| continue; |
| } |
| // compare selectors: |
| int relation = line.compareTo(selector); |
| if (relation > 0) { // past the insertion point |
| break; |
| } |
| if (relation < 0) { |
| newContent.append(line).append('\n'); |
| continue; |
| } |
| if (relation == 0) { |
| StringBuffer pending = new StringBuffer(line).append('\n'); |
| pending.append(line = reader.readLine()); |
| if (line == null) { |
| break; // found only the selector at EOF, append right here, ignoring 'pending' |
| } |
| // compare original signatures: |
| relation = line.trim().compareTo(originalSignature); |
| if (relation > 0) { // past the insertion point |
| // add new entry (below) |
| line = pending.toString(); // push back |
| break; |
| } |
| newContent.append(pending).append('\n'); |
| if (relation < 0) |
| continue; |
| if (relation == 0) { |
| // update existing entry: |
| String annotationLine = reader.readLine(); |
| String nextLine = null; |
| if (annotationLine == null || annotationLine.isEmpty() || !annotationLine.startsWith(" ")) { //$NON-NLS-1$ |
| nextLine = annotationLine; // push back, since not a signature line |
| annotationLine = line; // no annotated line yet, use unannotated line instead |
| } |
| if (annotationLine.startsWith(" ")) { //$NON-NLS-1$ |
| switch (mergeStrategy) { |
| case REPLACE_SIGNATURE: |
| break; // unconditionally use annotatedSignature |
| case OVERWRITE_ANNOTATIONS: |
| case ADD_ANNOTATIONS: |
| annotatedSignature = updateSignature(annotationLine.trim(), annotatedSignature, updatePosition, mergeStrategy); |
| break; |
| default: |
| JavaCore.getJavaCore().getLog().log(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, |
| "Unexpected value for enum MergeStrategy")); //$NON-NLS-1$ |
| } |
| } |
| writeFile(file, newContent, annotatedSignature, nextLine, reader, monitor); |
| return; |
| } |
| } |
| } |
| // add new entry: |
| newContent.append(selector).append('\n'); |
| newContent.append(' ').append(originalSignature).append('\n'); |
| annotatedSignature = updateSignature(originalSignature, annotatedSignature, updatePosition, mergeStrategy); |
| writeFile(file, newContent, annotatedSignature, line, reader, monitor); |
| } finally { |
| reader.close(); |
| } |
| } |
| } |
| |
| private static String updateSignature(String originalSignature, String annotatedSignature, int updatePosition, MergeStrategy mergeStrategy) { |
| StringBuffer buf = new StringBuffer(); |
| String signatureToReplace; |
| String postfix = null; |
| switch (updatePosition) { |
| case POSITION_FULL_SIGNATURE: |
| signatureToReplace = originalSignature; |
| break; |
| case POSITION_RETURN_TYPE: |
| assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ |
| int close = originalSignature.indexOf(')'); |
| buf.append(originalSignature, 0, close+1); |
| signatureToReplace = originalSignature.substring(close+1); |
| break; |
| default: // parameter |
| SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations |
| wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skipping type parameters |
| for (int i = 0; i < updatePosition; i++) |
| wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; |
| int start = wrapper.start; |
| int end = wrapper.skipAngleContents(wrapper.computeEnd()); |
| buf.append(originalSignature, 0, start); |
| signatureToReplace = originalSignature.substring(start, end+1); |
| postfix = originalSignature.substring(end+1, originalSignature.length()); |
| } |
| updateType(buf, signatureToReplace.toCharArray(), annotatedSignature.toCharArray(), mergeStrategy); |
| if (postfix != null) |
| buf.append(postfix); |
| return buf.toString(); |
| } |
| |
| /** |
| * Insert that given annotation at the given position into the given signature. |
| * @param mergeStrategy if set to {@link MergeStrategy#ADD_ANNOTATIONS}, refuse to |
| * overwrite any existing annotation in the specified location. |
| */ |
| private static String insertAt(String signature, int position, char annotation, MergeStrategy mergeStrategy) { |
| StringBuffer result = new StringBuffer(); |
| result.append(signature, 0, position); |
| result.append(annotation); |
| char next = signature.charAt(position); |
| switch (next) { |
| case NULLABLE: case NONNULL: |
| if (mergeStrategy == MergeStrategy.ADD_ANNOTATIONS) |
| return signature; // refuse any change |
| position++; // skip old annotation |
| } |
| result.append(signature, position, signature.length()); |
| return result.toString(); |
| } |
| |
| /** |
| * Update 'oldType' with annotations from 'newType' guided by 'mergeStrategy'. |
| * The result is written into 'buf' as we go. |
| */ |
| private static boolean updateType(StringBuffer buf, char[] oldType, char[] newType, MergeStrategy mergeStrategy) { |
| if (mergeStrategy == MergeStrategy.REPLACE_SIGNATURE) { |
| buf.append(newType); |
| return false; |
| } |
| try { |
| SignatureWrapper oWrap = new SignatureWrapper(oldType, true, true); // may already contain annotations |
| SignatureWrapper nWrap = new SignatureWrapper(newType, true, true); // may already contain annotations |
| if (match(buf, oWrap, nWrap, 'L', false) |
| || match(buf, oWrap, nWrap, 'T', false)) |
| { |
| mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); |
| buf.append(oWrap.nextName()); |
| nWrap.nextName(); // skip |
| if (match(buf, oWrap, nWrap, '<', false)) { |
| do { |
| int oStart = oWrap.start; |
| int nStart = nWrap.start; |
| oWrap.computeEnd(); |
| nWrap.computeEnd(); |
| if (updateType(buf, oWrap.getFrom(oStart), nWrap.getFrom(nStart), mergeStrategy)) |
| mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); |
| } while (!match(buf, oWrap, nWrap, '>', false)); |
| } |
| match(buf, oWrap, nWrap, ';', true); |
| } else if (match(buf, oWrap, nWrap, '[', false)) { |
| mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); |
| updateType(buf, oWrap.tail(), nWrap.tail(), mergeStrategy); |
| } else if (match(buf, oWrap, nWrap, '*', false) |
| || match(buf, oWrap, nWrap, '+', false) |
| || match(buf, oWrap, nWrap, '-', false)) |
| { |
| return true; // annotation allowed after this (not included in oldType / newType) |
| } else { |
| buf.append(oldType); |
| } |
| } catch (ArrayIndexOutOfBoundsException aioobe) { // from several locations inside match() or mergeAnnotation(). |
| StringBuilder msg = new StringBuilder("Structural mismatch between ").append(oldType).append(" and ").append(newType); //$NON-NLS-1$ //$NON-NLS-2$ |
| throw new IllegalArgumentException(msg.toString(), aioobe); |
| } |
| return false; |
| } |
| /** |
| * Does the current char at both given signatures match the 'expected' char? |
| * If yes, print it into 'buf' and answer true. |
| * If no, if 'force' raise an exception, else quietly answer false without updating 'buf'. |
| */ |
| private static boolean match(StringBuffer buf, SignatureWrapper sig1, SignatureWrapper sig2, char expected, boolean force) { |
| boolean match1 = sig1.signature[sig1.start] == expected; |
| boolean match2 = sig2.signature[sig2.start] == expected; |
| if (match1 != match2) { |
| StringBuilder msg = new StringBuilder("Mismatching type structures ") //$NON-NLS-1$ |
| .append(sig1.signature).append(" vs ").append(sig2.signature); //$NON-NLS-1$ |
| throw new IllegalArgumentException(msg.toString()); |
| } |
| if (match1) { |
| buf.append(expected); |
| sig1.start++; |
| sig2.start++; |
| return true; |
| } else if (force) { |
| throw new IllegalArgumentException("Expected char "+expected+" not found in "+new String(sig1.signature)); //$NON-NLS-1$ //$NON-NLS-2$ |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * If a current char of 'oldS' and/or 'newS' represents a null annotation, insert it into 'buf' guided by 'mergeStrategy'. |
| * If the new char is NO_ANNOTATION and strategy is OVERWRITE_ANNOTATIONS, silently skip over any null annotations in 'oldS'. |
| */ |
| private static void mergeAnnotation(StringBuffer buf, SignatureWrapper oldS, SignatureWrapper newS, MergeStrategy mergeStrategy) { |
| // if atEnd use a char that's different from NULLABLE, NONNULL and NO_ANNOTATION: |
| char oldAnn = !oldS.atEnd() ? oldS.signature[oldS.start] : '\0'; |
| char newAnn = !newS.atEnd() ? newS.signature[newS.start] : '\0'; |
| switch (mergeStrategy) { |
| case ADD_ANNOTATIONS: |
| switch (oldAnn) { |
| case NULLABLE: case NONNULL: |
| oldS.start++; |
| buf.append(oldAnn); // old exists, so it remains |
| switch (newAnn) { case NULLABLE: case NONNULL: newS.start++; } // just skip |
| return; |
| } |
| //$FALL-THROUGH$ |
| case OVERWRITE_ANNOTATIONS: |
| switch (newAnn) { |
| case NULLABLE: case NONNULL: |
| newS.start++; |
| buf.append(newAnn); // new exists and is not suppressed by "ADD & old exists" |
| switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; } // just skip |
| break; |
| case NO_ANNOTATION: |
| newS.start++; // don't insert |
| switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; } // just skip |
| break; |
| default: |
| switch (oldAnn) { |
| case NULLABLE: case NONNULL: |
| oldS.start++; |
| buf.append(oldAnn); // keep |
| } |
| } |
| break; |
| default: |
| throw new IllegalArgumentException("Unexpected merge strategy"); // REPLACE_SIGNATURE does not reach this point, see initial check in updateType() //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Write back the given annotationFile, with the following content: |
| * - head (assumed to include a member and its original signature |
| * - annotatedSignature |
| * - nextLines (optionally, may be null) |
| * - the still unconsumed content of tailReader |
| */ |
| private static void writeFile(IFile annotationFile, StringBuffer head, String annotatedSignature, |
| String nextLines, BufferedReader tailReader, IProgressMonitor monitor) |
| throws CoreException, IOException |
| { |
| head.append(' ').append(annotatedSignature).append('\n'); |
| if (nextLines != null) |
| head.append(nextLines).append('\n'); |
| String line; |
| while ((line = tailReader.readLine()) != null) |
| head.append(line).append('\n'); |
| ByteArrayInputStream newContent = new ByteArrayInputStream(head.toString().getBytes("UTF-8")); //$NON-NLS-1$ |
| annotationFile.setContents(newContent, IResource.KEEP_HISTORY, monitor); |
| } |
| |
| private static void createNewFile(IFile file, String newContent, IProgressMonitor monitor) throws CoreException { |
| ensureExists(file.getParent(), monitor); |
| |
| try { |
| file.create(new ByteArrayInputStream(newContent.getBytes("UTF-8")), false, monitor); //$NON-NLS-1$ |
| } catch (UnsupportedEncodingException e) { |
| throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, e.getMessage(), e)); |
| } |
| } |
| |
| private static void ensureExists(IContainer container, IProgressMonitor monitor) throws CoreException { |
| if (container.exists()) return; |
| if (!(container instanceof IFolder)) throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, "not a folder: "+container)); //$NON-NLS-1$ |
| IContainer parent= container.getParent(); |
| if (parent instanceof IFolder) { |
| ensureExists(parent, monitor); |
| } |
| ((IFolder) container).create(false, true, monitor); |
| } |
| |
| /** |
| * Retrieve the annotated signature of a specified member as found in the given external annotation file, if any. |
| * @param typeName fully qualified slash-separated name of the type for which the file defines external annotations |
| * @param file a file assumed to be in .eea format, must not be null, but may not exist |
| * @param selector name of the member whose annotation we are looking for |
| * @param originalSignature the unannotated signature by which the member is identified |
| * @return the annotated signature as found in the file, or null. |
| */ |
| public static String getAnnotatedSignature(String typeName, IFile file, String selector, String originalSignature) { |
| if (file.exists()) { |
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents()))) { |
| ExternalAnnotationProvider.assertClassHeader(reader.readLine(), typeName); |
| while (true) { |
| String line = reader.readLine(); |
| // selector: |
| if (selector.equals(line)) { |
| // original signature: |
| line = reader.readLine(); |
| if (originalSignature.equals(ExternalAnnotationProvider.extractSignature(line))) { |
| // annotated signature: |
| return ExternalAnnotationProvider.extractSignature(reader.readLine()); |
| } |
| } |
| if (line == null) |
| break; |
| } |
| } catch (IOException | CoreException e) { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Apply the specified changes on the given type. |
| * This method can be used as a dry run without modifying an annotation file. |
| * |
| * @param originalSignature the original type signature, may be annotated already |
| * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @return an array of length four: <ul> |
| * <li>prefix up-to the changed type</li> |
| * <li>original type</li> |
| * <li>changed type</li> |
| * <li>postfix after the changed type <em>(here: empty string)</li> |
| * </ul> |
| */ |
| public static String[] annotateType(String originalSignature, String annotatedType, MergeStrategy mergeStrategy) |
| { |
| String[] result = new String[4]; // prefix, orig, replacement, postfix |
| StringBuffer buf; |
| result[0] = ""; //$NON-NLS-1$ |
| buf = new StringBuffer(); |
| result[1] = originalSignature; |
| updateType(buf, originalSignature.toCharArray(), annotatedType.toCharArray(), mergeStrategy); |
| result[2] = buf.toString(); |
| result[3] = ""; //$NON-NLS-1$ |
| return result; |
| } |
| |
| /** |
| * Apply the specified changes on the return type of the given signature. |
| * This method can be used as a dry run without modifying an annotation file. |
| * |
| * @param originalSignature the original full signature, may be annotated already |
| * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @return an array of length four: <ul> |
| * <li>prefix up-to the changed type</li> |
| * <li>original type</li> |
| * <li>changed type</li> |
| * <li>postfix after the changed type <em>(here: empty string)</li> |
| * </ul> |
| */ |
| public static String[] annotateReturnType(String originalSignature, String annotatedType, MergeStrategy mergeStrategy) |
| { |
| String[] result = new String[4]; // prefix, orig, replacement, postfix |
| StringBuffer buf; |
| assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ |
| int close = originalSignature.indexOf(')'); |
| result[0] = originalSignature.substring(0, close+1); |
| buf = new StringBuffer(); |
| result[1] = originalSignature.substring(close+1); |
| updateType(buf, result[1].toCharArray(), annotatedType.toCharArray(), mergeStrategy); |
| result[2] = buf.toString(); |
| result[3] = ""; //$NON-NLS-1$ |
| return result; |
| } |
| |
| |
| /** |
| * Apply the specified changes on a parameter within the given signature. |
| * This method can be used as a dry run without modifying an annotation file. |
| * |
| * @param originalSignature the original full signature, may be annotated already |
| * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). |
| * @param paramIdx the index of a parameter to annotated |
| * @param mergeStrategy controls how old and new signatures should be merged |
| * @return an array of length four: <ul> |
| * <li>prefix up-to the changed type</li> |
| * <li>original type</li> |
| * <li>changed type</li> |
| * <li>postfix after the changed type</li> |
| * </ul> |
| */ |
| public static String[] annotateParameterType(String originalSignature, String annotatedType, int paramIdx, MergeStrategy mergeStrategy) |
| { |
| String[] result = new String[4]; // prefix, orig, replacement, postfix |
| StringBuffer buf; |
| SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations |
| wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skip type parameters |
| for (int i = 0; i < paramIdx; i++) |
| wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; |
| int start = wrapper.start; |
| int end = wrapper.skipAngleContents(wrapper.computeEnd()); |
| result[0] = originalSignature.substring(0, start); |
| buf = new StringBuffer(); |
| result[1] = originalSignature.substring(start, end+1); |
| updateType(buf, result[1].toCharArray(), annotatedType.toCharArray(), mergeStrategy); |
| result[2] = buf.toString(); |
| result[3] = originalSignature.substring(end+1, originalSignature.length()); |
| return result; |
| } |
| } |