/* * Copyright 2015 Lukas Krejci * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package org.revapi.java.spi; import static org.revapi.CompatibilityType.BINARY; import static org.revapi.CompatibilityType.SEMANTIC; import static org.revapi.CompatibilityType.SOURCE; import static org.revapi.DifferenceSeverity.BREAKING; import static org.revapi.DifferenceSeverity.EQUIVALENT; import static org.revapi.DifferenceSeverity.NON_BREAKING; import static org.revapi.DifferenceSeverity.POTENTIALLY_BREAKING; import java.lang.ref.WeakReference; import java.text.MessageFormat; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.WeakHashMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import org.revapi.CompatibilityType; import org.revapi.Difference; import org.revapi.DifferenceSeverity; /** * The is a list of all difference codes Revapi's Java extension can emit. This can be used by others when they want to * override the default detection behavior by providing custom difference transforms. * * @author Lukas Krejci * @since 0.1 */ public enum Code { //these two are used during class tree initialization even before any "normal" checks can occur MISSING_IN_OLD_API("java.missing.oldClass", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), MISSING_IN_NEW_API("java.missing.newClass", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), ELEMENT_NO_LONGER_DEPRECATED("java.element.noLongerDeprecated", EQUIVALENT, EQUIVALENT, null), ELEMENT_NOW_DEPRECATED("java.element.nowDeprecated", EQUIVALENT, EQUIVALENT, null), CLASS_VISIBILITY_INCREASED("java.class.visibilityIncreased", EQUIVALENT, EQUIVALENT, null), CLASS_VISIBILITY_REDUCED("java.class.visibilityReduced", BREAKING, BREAKING, null), CLASS_KIND_CHANGED("java.class.kindChanged", BREAKING, BREAKING, null), CLASS_NO_LONGER_FINAL("java.class.noLongerFinal", EQUIVALENT, EQUIVALENT, null), CLASS_NOW_FINAL("java.class.nowFinal", BREAKING, BREAKING, null), CLASS_NO_LONGER_ABSTRACT("java.class.noLongerAbstract", EQUIVALENT, EQUIVALENT, null), CLASS_NOW_ABSTRACT("java.class.nowAbstract", BREAKING, BREAKING, null), CLASS_ADDED("java.class.added", NON_BREAKING, NON_BREAKING, null), CLASS_REMOVED("java.class.removed", BREAKING, BREAKING, null), CLASS_NO_LONGER_IMPLEMENTS_INTERFACE("java.class.noLongerImplementsInterface", BREAKING, BREAKING, null), CLASS_NOW_IMPLEMENTS_INTERFACE("java.class.nowImplementsInterface", NON_BREAKING, NON_BREAKING, null), CLASS_FINAL_CLASS_INHERITS_FROM_NEW_CLASS("java.class.finalClassInheritsFromNewClass", EQUIVALENT, EQUIVALENT, null), CLASS_NON_FINAL_CLASS_INHERITS_FROM_NEW_CLASS("java.class.nonFinalClassInheritsFromNewClass", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), CLASS_NOW_CHECKED_EXCEPTION("java.class.nowCheckedException", BREAKING, NON_BREAKING, null), CLASS_NO_LONGER_INHERITS_FROM_CLASS("java.class.noLongerInheritsFromClass", BREAKING, BREAKING, null), CLASS_NON_PUBLIC_PART_OF_API("java.class.nonPublicPartOfAPI", NON_BREAKING, NON_BREAKING, BREAKING), CLASS_SUPER_TYPE_TYPE_PARAMETERS_CHANGED("java.class.superTypeTypeParametersChanged", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), CLASS_EXTERNAL_CLASS_EXPOSED_IN_API("java.class.externalClassExposedInAPI", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), CLASS_EXTERNAL_CLASS_NO_LONGER_EXPOSED_IN_API("java.class.externalClassNoLongerExposedInAPI", NON_BREAKING, NON_BREAKING, null), ANNOTATION_ADDED("java.annotation.added", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), ANNOTATION_REMOVED("java.annotation.removed", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), ANNOTATION_ATTRIBUTE_VALUE_CHANGED("java.annotation.attributeValueChanged", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), ANNOTATION_ATTRIBUTE_ADDED("java.annotation.attributeAdded", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), ANNOTATION_ATTRIBUTE_REMOVED("java.annotation.attributeRemoved", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), ANNOTATION_NO_LONGER_INHERITED("java.annotation.noLongerInherited", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), ANNOTATION_NOW_INHERITED("java.annotation.nowInherited", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), ANNOTATION_NO_LONGER_PRESENT("java.annotation.noLongerPresent", BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), FIELD_ADDED_STATIC_FIELD("java.field.addedStaticField", NON_BREAKING, NON_BREAKING, null), FIELD_ADDED("java.field.added", NON_BREAKING, NON_BREAKING, null), FIELD_REMOVED("java.field.removed", BREAKING, BREAKING, null), FIELD_MOVED_TO_SUPER_CLASS("java.field.movedToSuperClass", EQUIVALENT, EQUIVALENT, null), FIELD_INHERITED_NOW_DECLARED("java.field.inheritedNowDeclared", EQUIVALENT, EQUIVALENT, null), FIELD_CONSTANT_REMOVED("java.field.removedWithConstant", BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), FIELD_CONSTANT_VALUE_CHANGED("java.field.constantValueChanged", NON_BREAKING, NON_BREAKING, BREAKING), FIELD_NOW_CONSTANT("java.field.nowConstant", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), FIELD_NO_LONGER_CONSTANT("java.field.noLongerConstant", EQUIVALENT, EQUIVALENT, BREAKING), FIELD_NOW_FINAL("java.field.nowFinal", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), FIELD_NO_LONGER_FINAL("java.field.noLongerFinal", NON_BREAKING, NON_BREAKING, null), FIELD_NO_LONGER_STATIC("java.field.noLongerStatic", BREAKING, BREAKING, null), FIELD_NOW_STATIC("java.field.nowStatic", NON_BREAKING, BREAKING, null), FIELD_TYPE_CHANGED("java.field.typeChanged", BREAKING, BREAKING, null), FIELD_SERIAL_VERSION_UID_UNCHANGED("java.field.serialVersionUIDUnchanged", EQUIVALENT, EQUIVALENT, POTENTIALLY_BREAKING), FIELD_VISIBILITY_INCREASED("java.field.visibilityIncreased", EQUIVALENT, EQUIVALENT, null), FIELD_VISIBILITY_REDUCED("java.field.visibilityReduced", BREAKING, BREAKING, null), FIELD_ENUM_CONSTANT_ORDER_CHANGED("java.field.enumConstantOrderChanged", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), METHOD_DEFAULT_VALUE_ADDED("java.method.defaultValueAdded", NON_BREAKING, NON_BREAKING, null), METHOD_DEFAULT_VALUE_CHANGED("java.method.defaultValueChanged", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), METHOD_DEFAULT_VALUE_REMOVED("java.method.defaultValueRemoved", BREAKING, NON_BREAKING, BREAKING), METHOD_ADDED_TO_INTERFACE("java.method.addedToInterface", BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), METHOD_DEFAULT_METHOD_ADDED_TO_INTERFACE("java.method.defaultMethodAddedToInterface", NON_BREAKING, NON_BREAKING, null), METHOD_STATIC_METHOD_ADDED_TO_INTERFACE("java.method.staticMethodAddedToInterface", NON_BREAKING, NON_BREAKING, null), METHOD_ATTRIBUTE_WITH_NO_DEFAULT_ADDED_TO_ANNOTATION_TYPE("java.method.attributeWithNoDefaultAddedToAnnotationType", BREAKING, NON_BREAKING, BREAKING), METHOD_ATTRIBUTE_WITH_DEFAULT_ADDED_TO_ANNOTATION_TYPE("java.method.attributeWithDefaultAddedToAnnotationType", NON_BREAKING, NON_BREAKING, null), METHOD_ABSTRACT_METHOD_ADDED("java.method.abstractMethodAdded", BREAKING, BREAKING, null), METHOD_ADDED("java.method.added", NON_BREAKING, NON_BREAKING, null), METHOD_FINAL_METHOD_ADDED_TO_NON_FINAL_CLASS("java.method.finalMethodAddedToNonFinalClass", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), METHOD_REMOVED("java.method.removed", BREAKING, BREAKING, null), METHOD_MOVED_TO_SUPERCLASS("java.method.movedToSuperClass", EQUIVALENT, EQUIVALENT, null), METHOD_INHERITED_METHOD_MOVED_TO_CLASS("java.method.inheritedMovedToClass", EQUIVALENT, EQUIVALENT, null), METHOD_ATTRIBUTE_REMOVED_FROM_ANNOTATION_TYPE( "java.method.attributeRemovedFromAnnotationType", BREAKING, BREAKING, null), METHOD_NO_LONGER_FINAL("java.method.noLongerFinal", NON_BREAKING, NON_BREAKING, null), METHOD_NOW_FINAL("java.method.nowFinal", POTENTIALLY_BREAKING, POTENTIALLY_BREAKING, null), METHOD_NOW_FINAL_IN_FINAL_CLASS("java.method.nowFinalInFinalClass", EQUIVALENT, EQUIVALENT, null), METHOD_VISIBILITY_INCREASED("java.method.visibilityIncreased", EQUIVALENT, EQUIVALENT, null), METHOD_VISIBILITY_REDUCED("java.method.visibilityReduced", BREAKING, BREAKING, null), METHOD_RETURN_TYPE_CHANGED("java.method.returnTypeChanged", POTENTIALLY_BREAKING, BREAKING, null), METHOD_RETURN_TYPE_TYPE_PARAMETERS_CHANGED("java.method.returnTypeTypeParametersChanged", BREAKING, NON_BREAKING, null), METHOD_RETURN_TYPE_CHANGED_COVARIANTLY("java.method.returnTypeChangedCovariantly", NON_BREAKING, NON_BREAKING, null), METHOD_NUMBER_OF_PARAMETERS_CHANGED("java.method.numberOfParametersChanged", BREAKING, BREAKING, null), METHOD_PARAMETER_TYPE_CHANGED("java.method.parameterTypeChanged", POTENTIALLY_BREAKING, BREAKING, null), METHOD_PARAMETER_TYPE_PARAMETER_CHANGED("java.method.parameterTypeParameterChanged", POTENTIALLY_BREAKING, NON_BREAKING, null), METHOD_NO_LONGER_STATIC("java.method.noLongerStatic", BREAKING, BREAKING, null), METHOD_NOW_STATIC("java.method.nowStatic", NON_BREAKING, BREAKING, null), METHOD_CHECKED_EXCEPTION_ADDED("java.method.exception.checkedAdded", BREAKING, NON_BREAKING, null), METHOD_RUNTIME_EXCEPTION_ADDED("java.method.exception.runtimeAdded", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), METHOD_CHECKED_EXCEPTION_REMOVED("java.method.exception.checkedRemoved", BREAKING, NON_BREAKING, null), METHOD_RUNTIME_EXCEPTION_REMOVED("java.method.exception.runtimeRemoved", NON_BREAKING, NON_BREAKING, null), METHOD_NO_LONGER_DEFAULT("java.method.noLongerDefault", BREAKING, BREAKING, null), METHOD_NOW_DEFAULT("java.method.nowDefault", EQUIVALENT, EQUIVALENT, null), METHOD_NOW_ABSTRACT("java.method.nowAbstract", BREAKING, BREAKING, null), METHOD_NO_LONGER_ABSTRACT("java.method.noLongerAbstract", EQUIVALENT, EQUIVALENT, null), GENERICS_ELEMENT_NOW_PARAMETERIZED("java.generics.elementNowParameterized", NON_BREAKING, NON_BREAKING, POTENTIALLY_BREAKING), GENERICS_FORMAL_TYPE_PARAMETER_ADDED("java.generics.formalTypeParameterAdded", BREAKING, NON_BREAKING, null), GENERICS_FORMAL_TYPE_PARAMETER_REMOVED("java.generics.formalTypeParameterRemoved", BREAKING, NON_BREAKING, null), GENERICS_FORMAL_TYPE_PARAMETER_CHANGED("java.generics.formalTypeParameterChanged", BREAKING, NON_BREAKING, null); private final String code; private final EnumMap<CompatibilityType, DifferenceSeverity> classification; Code(String code, DifferenceSeverity sourceSeverity, DifferenceSeverity binarySeverity, DifferenceSeverity semanticSeverity) { this.code = code; classification = new EnumMap<>(CompatibilityType.class); addClassification(SOURCE, sourceSeverity); addClassification(BINARY, binarySeverity); addClassification(SEMANTIC, semanticSeverity); } @SuppressWarnings("UnusedDeclaration") public static Code fromCode(String code) { for (Code c : Code.values()) { if (c.code.equals(code)) { return c; } } return null; } public static <T extends JavaElement> String[] attachmentsFor(@Nullable T oldElement, @Nullable T newElement, String... customAttachments) { T representative = oldElement == null ? newElement : oldElement; if (representative == null) { throw new IllegalArgumentException("At least one of the oldElement and newElement must not be null"); } int idx = customAttachments.length; if (representative instanceof JavaAnnotationElement) { //annotationType JavaAnnotationElement anno = representative.as(JavaAnnotationElement.class); String[] ret = new String[customAttachments.length + 2]; System.arraycopy(customAttachments, 0, ret, 0, customAttachments.length); ret[idx] = "annotationType"; ret[idx + 1] = Util.toHumanReadableString(anno.getAnnotation().getAnnotationType()); return ret; } else if (representative instanceof JavaFieldElement) { //package, classSimpleName, fieldName JavaFieldElement field = representative.as(JavaFieldElement.class); String[] ret = new String[customAttachments.length + 6]; System.arraycopy(customAttachments, 0, ret, 0, customAttachments.length); ret[idx] = "package"; ret[idx + 1] = getPackageName(field); ret[idx + 2] = "classSimpleName"; ret[idx + 3] = getClassSimpleName(field); ret[idx + 4] = "fieldName"; ret[idx + 5] = field.getDeclaringElement().getSimpleName().toString(); return ret; } else if (representative instanceof JavaTypeElement) { //package, classSimpleName JavaTypeElement type = representative.as(JavaTypeElement.class); String[] ret = new String[customAttachments.length + 4]; System.arraycopy(customAttachments, 0, ret, 0, customAttachments.length); ret[idx] = "package"; ret[idx + 1] = getPackageName(type); ret[idx + 2] = "classSimpleName"; ret[idx + 3] = getClassSimpleName(type); return ret; } else if (representative instanceof JavaMethodElement) { //package, classSimpleName, methodName JavaMethodElement method = representative.as(JavaMethodElement.class); String[] ret = new String[customAttachments.length + 6]; System.arraycopy(customAttachments, 0, ret, 0, customAttachments.length); ret[idx] = "package"; ret[idx + 1] = getPackageName(method); ret[idx + 2] = "classSimpleName"; ret[idx + 3] = getClassSimpleName(method); ret[idx + 4] = "methodName"; ret[idx + 5] = method.getDeclaringElement().getSimpleName().toString(); return ret; } else if (representative instanceof JavaMethodParameterElement) { //package, classSimpleName, methodName, parameterIndex JavaMethodParameterElement param = (JavaMethodParameterElement) representative; @SuppressWarnings("ConstantConditions") JavaMethodElement method = representative.getParent().as(JavaMethodElement.class); String[] ret = new String[customAttachments.length + 8]; System.arraycopy(customAttachments, 0, ret, 0, customAttachments.length); ret[idx] = "package"; ret[idx + 1] = getPackageName(method); ret[idx + 2] = "classSimpleName"; ret[idx + 3] = getClassSimpleName(method); ret[idx + 4] = "methodName"; ret[idx + 5] = method.getDeclaringElement().getSimpleName().toString(); ret[idx + 6] = "parameterIndex"; ret[idx + 7] = Integer.toString(param.getIndex()); return ret; } else { return customAttachments; } } private static String getPackageName(JavaModelElement element) { while (element != null && !(element instanceof JavaTypeElement)) { element = element.getParent(); } if (element == null) { return ""; } else { TypeElement type = element.as(JavaTypeElement.class).getDeclaringElement(); Element pkg = type.getEnclosingElement(); while (pkg != null && pkg.getKind() != ElementKind.PACKAGE) { pkg = pkg.getEnclosingElement(); } if (pkg == null) { return ""; } else { return ((PackageElement) pkg).getQualifiedName().toString(); } } } private static String getClassSimpleName(JavaModelElement element) { while (element != null && !(element instanceof JavaTypeElement)) { element = element.getParent(); } if (element == null) { return null; } else { return element.as(JavaTypeElement.class).getDeclaringElement().getSimpleName().toString(); } } public String code() { return code; } public Difference createDifference(@Nonnull Locale locale) { Message message = getMessages(locale).get(code); Difference.Builder bld = Difference.builder().withCode(code).withName(message.name) .withDescription(message.description); for (Map.Entry<CompatibilityType, DifferenceSeverity> e : classification.entrySet()) { bld.addClassification(e.getKey(), e.getValue()); } return bld.build(); } public Difference createDifference(@Nonnull Locale locale, String... attachments) { return createDifference(locale, keyVals(attachments)); } public Difference createDifference(@Nonnull Locale locale, LinkedHashMap<String, String> attachments) { String[] params = attachments.values().toArray(new String[attachments.size()]); return createDifference(locale, attachments, params); } private static String[] vals(String... keyVals) { if (keyVals.length % 2 != 0) { throw new IllegalArgumentException("Uneven key-value pairs."); } String[] ret = new String[keyVals.length / 2]; for (int i = 1; i < keyVals.length; i += 2) { ret[(i - 1) / 2] = keyVals[i]; } return ret; } public Difference createDifferenceWithExplicitParams(@Nonnull Locale locale, String[] attachments, String... params) { LinkedHashMap<String, String> ats = keyVals(attachments); return createDifference(locale, ats, params); } private Difference createDifference(@Nonnull Locale locale, LinkedHashMap<String, String> attachments, String... parameters) { Message message = getMessages(locale).get(code); String description = MessageFormat.format(message.description, parameters); Difference.Builder bld = Difference.builder().withCode(code).withName(message.name) .withDescription(description).addAttachments(attachments); for (Map.Entry<CompatibilityType, DifferenceSeverity> e : classification.entrySet()) { bld.addClassification(e.getKey(), e.getValue()); } return bld.build(); } private static LinkedHashMap<String, String> keyVals(String... keyVals) { if (keyVals.length % 2 != 0) { throw new IllegalArgumentException("Uneven key-value pairs."); } LinkedHashMap<String, String> ret = new LinkedHashMap<>(keyVals.length / 2); String currentKey = null; for (int i = 0; i < keyVals.length; ++i) { String x = keyVals[i]; if (x == null) { throw new IllegalArgumentException("Null keys or values not supported in attachments."); } if (i % 2 == 0) { currentKey = keyVals[i]; } else { ret.put(currentKey, x); } } return ret; } private static class Message { final String name; final String description; private Message(String name, String description) { this.description = description; this.name = name; } } private static class Messages { private final ResourceBundle names; private final ResourceBundle descriptions; public Messages(Locale locale) { descriptions = ResourceBundle.getBundle("org.revapi.java.checks.descriptions", locale); names = ResourceBundle.getBundle("org.revapi.java.checks.names", locale); } Message get(String key) { String name = names.getString(key); String description = descriptions.getString(key); return new Message(name, description); } } private static WeakHashMap<Locale, WeakReference<Messages>> messagesCache = new WeakHashMap<>(); private static synchronized Messages getMessages(Locale locale) { WeakReference<Messages> messageRef = messagesCache.get(locale); if (messageRef == null || messageRef.get() == null) { messageRef = new WeakReference<>(new Messages(locale)); messagesCache.put(locale, messageRef); } return messageRef.get(); } private void addClassification(CompatibilityType compatibilityType, DifferenceSeverity severity) { if (severity != null) { classification.put(compatibilityType, severity); } } }