diff --git a/build.gradle b/build.gradle index 15fb87227b..c8a38eaec4 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,8 @@ plugins { id 'org.hibernate.orm.database-service' apply false id 'biz.aQute.bnd' version '6.3.1' apply false + id 'org.checkerframework' version '0.6.25' + id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' id 'idea' diff --git a/checkerstubs/jakarta.persistence.astub b/checkerstubs/jakarta.persistence.astub new file mode 100644 index 0000000000..55415ca015 --- /dev/null +++ b/checkerstubs/jakarta.persistence.astub @@ -0,0 +1,166 @@ +// Checkerframework stubs for the jakarta.persistence module + +package jakarta.persistence; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface AttributeConverter { + public @Nullable Y convertToDatabaseColumn(@Nullable X attribute); + public @Nullable X convertToEntityAttribute(@Nullable Y dbData); +} +public interface EntityManager extends AutoCloseable { + public @Nullable T find(Class entityClass, Object primaryKey); + public @Nullable T find(Class entityClass, Object primaryKey, Map properties); + public @Nullable T find(Class entityClass, Object primaryKey, LockModeType lockMode); + public @Nullable T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties); +} +public interface EntityManagerFactory extends AutoCloseable { + public @Nullable Cache getCache(); +} +public interface Parameter { + public @Nullable String getName(); + public @Nullable Integer getPosition(); +} +public interface PersistenceUnitUtil extends PersistenceUtil { + public @Nullable Object getIdentifier(Object entity); +} +public interface Query { + Query setParameter(Parameter param, @Nullable T value); + Query setParameter(Parameter param, @Nullable Calendar value, TemporalType temporalType); + Query setParameter(Parameter param, @Nullable Date value, TemporalType temporalType); + Query setParameter(String name, @Nullable Object value); + Query setParameter(String name, @Nullable Calendar value, TemporalType temporalType); + Query setParameter(String name, @Nullable Date value, TemporalType temporalType); + Query setParameter(int position, @Nullable Object value); + Query setParameter(int position, @Nullable Calendar value, TemporalType temporalType); + Query setParameter(int position, @Nullable Date value, TemporalType temporalType); + @Nullable T getParameterValue(Parameter param); + @Nullable Object getParameterValue(String name); + @Nullable Object getParameterValue(int position); +} +public interface StoredProcedureQuery extends Query { + StoredProcedureQuery setParameter(Parameter param, @Nullable T value); + StoredProcedureQuery setParameter(Parameter param, @Nullable Calendar value, TemporalType temporalType); + StoredProcedureQuery setParameter(Parameter param, @Nullable Date value, TemporalType temporalType); + StoredProcedureQuery setParameter(String name, @Nullable Object value); + StoredProcedureQuery setParameter(String name, @Nullable Calendar value, TemporalType temporalType); + StoredProcedureQuery setParameter(String name, @Nullable Date value, TemporalType temporalType); + StoredProcedureQuery setParameter(int position, @Nullable Object value); + StoredProcedureQuery setParameter(int position, @Nullable Calendar value, TemporalType temporalType); + StoredProcedureQuery setParameter(int position, @Nullable Date value, TemporalType temporalType); + @Nullable Object getOutputParameterValue(int position); + @Nullable Object getOutputParameterValue(String parameterName); +} +public interface TypedQuery extends Query { + TypedQuery setParameter(Parameter param, @Nullable T value); + TypedQuery setParameter(Parameter param, @Nullable Calendar value, TemporalType temporalType); + TypedQuery setParameter(Parameter param, @Nullable Date value, TemporalType temporalType); + TypedQuery setParameter(String name, @Nullable Object value); + TypedQuery setParameter(String name, @Nullable Calendar value, TemporalType temporalType); + TypedQuery setParameter(String name, @Nullable Date value, TemporalType temporalType); + TypedQuery setParameter(int position, @Nullable Object value); + TypedQuery setParameter(int position, @Nullable Calendar value, TemporalType temporalType); + TypedQuery setParameter(int position, @Nullable Date value, TemporalType temporalType); + @Nullable Object getOutputParameterValue(int position); + @Nullable Object getOutputParameterValue(String parameterName); +} +public interface Tuple { + @Nullable X get(TupleElement tupleElement); + @Nullable X get(String alias, Class type); + @Nullable Object get(String alias); + @Nullable X get(int i, Class type); + @Nullable Object get(int i); + @Nullable Object[] toArray(); +} +public interface TupleElement { + @Nullable String getAlias(); +} + +package jakarta.persistence.criteria; + +public interface CommonAbstractCriteria { + @Nullable Predicate getRestriction(); +} +public interface AbstractQuery extends CommonAbstractCriteria { + AbstractQuery where(@Nullable Expression restriction); + AbstractQuery where(@Nullable Predicate... restrictions); + AbstractQuery having(@Nullable Expression restriction); + AbstractQuery having(@Nullable Predicate... restrictions); + @Nullable Selection getSelection(); + @Nullable Predicate getGroupRestriction(); +} +public interface CriteriaUpdate extends CommonAbstractCriteria { + CriteriaUpdate set(SingularAttribute attribute, @Nullable X value); + CriteriaUpdate set(Path attribute, @Nullable X value); + CriteriaUpdate set(String attributeName, @Nullable Object value); +} +public interface Subquery extends AbstractQuery, Expression { + Subquery where(@Nullable Expression restriction); + Subquery where(@Nullable Predicate... restrictions); + Subquery having(@Nullable Expression restriction); + Subquery having(@Nullable Predicate... restrictions); + @Nullable Expression getSelection(); +} +public interface CriteriaBuilder { + public static interface SimpleCase extends Expression { + SimpleCase when(C condition, @Nullable R result); + SimpleCase when(Expression condition, @Nullable R result); + Expression otherwise(@Nullable R result); + } + public static interface Case extends Expression { + Case when(Expression condition, @Nullable R result); + Expression otherwise(@Nullable R result); + } +} +public interface Join extends From { + Join on(@Nullable Expression restriction); + Join on(@Nullable Predicate... restrictions); + @Nullable Predicate getOn(); +} +public interface SetJoin extends PluralJoin, E> { + SetJoin on(@Nullable Expression restriction); + SetJoin on(@Nullable Predicate... restrictions); +} +public interface ListJoin extends PluralJoin, E> { + ListJoin on(@Nullable Expression restriction); + ListJoin on(@Nullable Predicate... restrictions); +} +public interface MapJoin extends PluralJoin, V> { + MapJoin on(@Nullable Expression restriction); + MapJoin on(@Nullable Predicate... restrictions); +} +public interface Path extends Expression { + // CteRoot etc. + @Nullable Bindable getModel(); + @Nullable Path getParentPath(); + MapJoin on(@Nullable Predicate... restrictions); +} + +package jakarta.persistence.metamodel; + +public interface IdentifiableType extends ManagedType { + @Nullable IdentifiableType getSupertype(); +} + +package jakarta.persistence.spi; + +public interface ClassTransformer { + @Nullable byte[] transform( + @Nullable ClassLoader loader, + String className, + @Nullable Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws TransformerException; +} +public interface PersistenceProvider { + public @Nullable EntityManagerFactory createEntityManagerFactory(String emName, @Nullable Map map); + public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, @Nullable Map map); +} +public interface PersistenceUnitInfo { + public @Nullable String getPersistenceProviderClassName(); + public @Nullable PersistenceUnitTransactionType getTransactionType(); + public @Nullable DataSource getJtaDataSource(); + public @Nullable DataSource getNonJtaDataSource(); + public @Nullable ClassLoader getClassLoader(); + public @Nullable ClassLoader getNewTempClassLoader(); +} \ No newline at end of file diff --git a/gradle/java-module.gradle b/gradle/java-module.gradle index 7f651fd7ee..eeb9b8a45b 100644 --- a/gradle/java-module.gradle +++ b/gradle/java-module.gradle @@ -31,6 +31,8 @@ apply plugin: 'java-library' apply plugin: 'biz.aQute.bnd.builder' apply plugin: 'org.hibernate.orm.database-service' +apply plugin: 'org.checkerframework' + apply plugin: 'checkstyle' apply plugin: 'build-dashboard' apply plugin: 'project-report' @@ -502,6 +504,9 @@ checkstyle { // exclude generated java sources - by explicitly setting the base source dir tasks.checkstyleMain.source = 'src/main/java' +tasks.checkstyleMain + .exclude('org/hibernate/jpamodelgen/util/NullnessUtil.java') + .exclude('org/hibernate/internal/util/NullnessUtil.java') // define a second checkstyle task for checking non-fatal violations task nonFatalCheckstyle(type:Checkstyle) { @@ -511,6 +516,17 @@ task nonFatalCheckstyle(type:Checkstyle) { configFile = rootProject.file( 'shared/config/checkstyle/checkstyle-non-fatal.xml' ) } +checkerFramework { + checkers = [ + 'org.checkerframework.checker.nullness.NullnessChecker' + ] + extraJavacArgs = [ + '-AsuppressWarnings=initialization', + "-Astubs=${project.rootDir}/checkerstubs", + '-AonlyDefs=^org\\.hibernate\\.jpamodelgen\\.' + ] +} + task forbiddenApisSystemOut(type: CheckForbiddenApis, dependsOn: compileJava) { bundledSignatures += 'jdk-system-out' diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/NullnessUtil.java b/hibernate-core/src/main/java/org/hibernate/internal/util/NullnessUtil.java new file mode 100644 index 0000000000..a40e72ddd9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/NullnessUtil.java @@ -0,0 +1,361 @@ +/* + * Checker Framework utilities + * Copyright 2004-present by the Checker Framework developers + * + * MIT License: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.hibernate.internal.util; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; + +/** + * Utility class for the Nullness Checker. + * + *

To avoid the need to write the NullnessUtil class name, do: + * + *

import static org.checkerframework.checker.nullness.util.NullnessUtil.castNonNull;
+ *

+ * or + * + *

import static org.checkerframework.checker.nullness.util.NullnessUtil.*;
+ * + *

Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can copy this class into your own project. + */ +@SuppressWarnings({ + "nullness", // Nullness utilities are trusted regarding nullness. + "cast" // Casts look redundant if Nullness Checker is not run. +}) +@AnnotatedFor("nullness") +public final class NullnessUtil { + + private NullnessUtil() { + throw new AssertionError( "shouldn't be instantiated" ); + } + + /** + * A method that suppresses warnings from the Nullness Checker. + * + *

The method takes a possibly-null reference, unsafely casts it to have the @NonNull type + * qualifier, and returns it. The Nullness Checker considers both the return value, and also the + * argument, to be non-null after the method call. Therefore, the {@code castNonNull} method can + * be used either as a cast expression or as a statement. The Nullness Checker issues no warnings + * in any of the following code: + * + *


+	 *   // one way to use as a cast:
+	 *  {@literal @}NonNull String s = castNonNull(possiblyNull1);
+	 *
+	 *   // another way to use as a cast:
+	 *   castNonNull(possiblyNull2).toString();
+	 *
+	 *   // one way to use as a statement:
+	 *   castNonNull(possiblyNull3);
+	 *   possiblyNull3.toString();`
+	 * 
+ *

+ * The {@code castNonNull} method is intended to be used in situations where the programmer + * definitively knows that a given reference is not null, but the type system is unable to make + * this deduction. It is not intended for defensive programming, in which a programmer cannot + * prove that the value is not null but wishes to have an earlier indication if it is. See the + * Checker Framework Manual for further discussion. + * + *

The method throws {@link AssertionError} if Java assertions are enabled and the argument is + * {@code null}. If the exception is ever thrown, then that indicates that the programmer misused + * the method by using it in a circumstance where its argument can be null. + * + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time + * + * @return the argument, casted to have the type qualifier @NonNull + */ + @EnsuresNonNull("#1") + public static @NonNull T castNonNull(@Nullable T ref) { + assert ref != null : "Misuse of castNonNull: called with a null argument"; + return (@NonNull T) ref; + } + + /** + * Suppress warnings from the Nullness Checker, with a custom error message. See {@link + * #castNonNull(Object)} for documentation. + * + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull + * + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") @NonNull T castNonNull( + @Nullable T ref, String message) { + assert ref != null : "Misuse of castNonNull: called with a null argument: " + message; + return (@NonNull T) ref; + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [] castNonNullDeep( + T @Nullable [] arr) { + return (@NonNull T[]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [] castNonNullDeep( + T @Nullable [] arr, String message) { + return (@NonNull T[]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][] castNonNullDeep( + T @Nullable [] @Nullable [] arr) { + return (@NonNull T[][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][] castNonNullDeep( + T @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][][][]) castNonNullArray( arr, message ); + } + + /** + * The implementation of castNonNullDeep. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + */ + private static @NonNull T @NonNull [] castNonNullArray( + T @Nullable [] arr, @Nullable String message) { + assert arr != null + : "Misuse of castNonNullArray: called with a null array argument" + + ( ( message == null ) ? "" : ( ": " + message ) ); + for ( int i = 0; i < arr.length; ++i ) { + assert arr[i] != null + : "Misuse of castNonNull: called with a null array element" + + ( ( message == null ) ? "" : ( ": " + message ) ); + checkIfArray( arr[i], message ); + } + return (@NonNull T[]) arr; + } + + /** + * If the argument is an array, requires it to be non-null at all levels. + * + * @param ref a value; if an array, all of its elements, and their elements recursively, are + * non-null at run time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + */ + private static void checkIfArray(@NonNull Object ref, @Nullable String message) { + assert ref != null + : "Misuse of checkIfArray: called with a null argument" + + ( ( message == null ) ? "" : ( ": " + message ) ); + Class comp = ref.getClass().getComponentType(); + if ( comp != null ) { + // comp is non-null for arrays, otherwise null. + if ( comp.isPrimitive() ) { + // Nothing to do for arrays of primitive type: primitives are + // never null. + } + else { + castNonNullArray( (Object[]) ref, message ); + } + } + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/ClassWriter.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/ClassWriter.java index a0c7976e45..5434a9a083 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/ClassWriter.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/ClassWriter.java @@ -27,6 +27,8 @@ import org.hibernate.jpamodelgen.model.MetaEntity; import org.hibernate.jpamodelgen.util.Constants; import org.hibernate.jpamodelgen.util.TypeUtils; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Helper class to write the actual meta model class using the {@link javax.annotation.processing.Filer} API. * @@ -35,6 +37,8 @@ import org.hibernate.jpamodelgen.util.TypeUtils; */ public final class ClassWriter { private static final String META_MODEL_CLASS_NAME_SUFFIX = "_"; + // See https://github.com/typetools/checker-framework/issues/979 + @SuppressWarnings( "type.argument" ) private static final ThreadLocal SIMPLE_DATE_FORMAT = new ThreadLocal() { @Override public SimpleDateFormat initialValue() { @@ -140,7 +144,7 @@ public final class ClassWriter { pw.println( " {" ); } - private static String findMappedSuperClass(MetaEntity entity, Context context) { + private static @Nullable String findMappedSuperClass(MetaEntity entity, Context context) { TypeMirror superClass = entity.getTypeElement().getSuperclass(); //superclass of Object is of NoType which returns some other kind while ( superClass.getKind() == TypeKind.DECLARED ) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Context.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Context.java index c09ec34288..2d7d5b8906 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Context.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Context.java @@ -24,6 +24,8 @@ import org.hibernate.jpamodelgen.util.AccessType; import org.hibernate.jpamodelgen.util.AccessTypeInformation; import org.hibernate.jpamodelgen.util.Constants; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Max Andersen * @author Hardy Ferentschik @@ -68,21 +70,21 @@ public final class Context { public Context(ProcessingEnvironment pe) { this.pe = pe; - if ( pe.getOptions().get( JPAMetaModelEntityProcessor.PERSISTENCE_XML_OPTION ) != null ) { - String tmp = pe.getOptions().get( JPAMetaModelEntityProcessor.PERSISTENCE_XML_OPTION ); - if ( !tmp.startsWith( Constants.PATH_SEPARATOR ) ) { - tmp = Constants.PATH_SEPARATOR + tmp; + String persistenceXmlOption = pe.getOptions().get( JPAMetaModelEntityProcessor.PERSISTENCE_XML_OPTION ); + if ( persistenceXmlOption != null ) { + if ( !persistenceXmlOption.startsWith( Constants.PATH_SEPARATOR ) ) { + persistenceXmlOption = Constants.PATH_SEPARATOR + persistenceXmlOption; } - persistenceXmlLocation = tmp; + persistenceXmlLocation = persistenceXmlOption; } else { persistenceXmlLocation = DEFAULT_PERSISTENCE_XML_LOCATION; } - if ( pe.getOptions().get( JPAMetaModelEntityProcessor.ORM_XML_OPTION ) != null ) { - String tmp = pe.getOptions().get( JPAMetaModelEntityProcessor.ORM_XML_OPTION ); - ormXmlFiles = new ArrayList(); - for ( String ormFile : tmp.split( "," ) ) { + String ormXmlOption = pe.getOptions().get( JPAMetaModelEntityProcessor.ORM_XML_OPTION ); + if ( ormXmlOption != null ) { + ormXmlFiles = new ArrayList<>(); + for ( String ormFile : ormXmlOption.split( "," ) ) { if ( !ormFile.startsWith( Constants.PATH_SEPARATOR ) ) { ormFile = Constants.PATH_SEPARATOR + ormFile; } @@ -161,7 +163,7 @@ public final class Context { return metaEntities.containsKey( fqcn ); } - public MetaEntity getMetaEntity(String fqcn) { + public @Nullable MetaEntity getMetaEntity(String fqcn) { return metaEntities.get( fqcn ); } @@ -177,7 +179,7 @@ public final class Context { return metaEmbeddables.containsKey( fqcn ); } - public MetaEntity getMetaEmbeddable(String fqcn) { + public @Nullable MetaEntity getMetaEmbeddable(String fqcn) { return metaEmbeddables.get( fqcn ); } @@ -193,7 +195,7 @@ public final class Context { accessTypeInformation.put( fqcn, info ); } - public AccessTypeInformation getAccessTypeInfo(String fqcn) { + public @Nullable AccessTypeInformation getAccessTypeInfo(String fqcn) { return accessTypeInformation.get( fqcn ); } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java index cb713288a3..3be15381be 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java @@ -36,6 +36,8 @@ import org.hibernate.jpamodelgen.util.StringUtil; import org.hibernate.jpamodelgen.util.TypeUtils; import org.hibernate.jpamodelgen.xml.JpaDescriptorParser; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Main annotation processor. * @@ -240,7 +242,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor { requiresLazyMemberInitialization = true; } - metaEntity = new AnnotationMetaEntity( (TypeElement) element, context, requiresLazyMemberInitialization ); + metaEntity = AnnotationMetaEntity.create( (TypeElement) element, context, requiresLazyMemberInitialization ); if ( alreadyExistingMetaEntity != null ) { metaEntity.mergeInMembers( alreadyExistingMetaEntity ); @@ -249,7 +251,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor { } } - private MetaEntity tryGettingExistingEntityFromContext(AnnotationMirror mirror, String fqn) { + private @Nullable MetaEntity tryGettingExistingEntityFromContext(AnnotationMirror mirror, String fqn) { MetaEntity alreadyExistingMetaEntity = null; if ( TypeUtils.isAnnotationMirrorOfType( mirror, Constants.ENTITY ) || TypeUtils.isAnnotationMirrorOfType( mirror, Constants.MAPPED_SUPERCLASS )) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Version.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Version.java index d1451dec1b..b51f03e98f 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Version.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/Version.java @@ -6,20 +6,24 @@ */ package org.hibernate.jpamodelgen; +import org.hibernate.jpamodelgen.util.NullnessUtil; + +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Information about the Meta Model Generator version. * * @author Hardy Ferentschik */ public final class Version { - private static String version; + private static @Nullable String version; private Version() { } public static String getVersionString() { if ( version == null ) { - version = Version.class.getPackage().getImplementationVersion(); + version = NullnessUtil.castNonNull( Version.class.getPackage() ).getImplementationVersion(); if ( version == null ) { version = "[WORKING]"; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java index 1db4af51fb..0389459c96 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java @@ -28,6 +28,7 @@ import org.hibernate.jpamodelgen.model.MetaEntity; import org.hibernate.jpamodelgen.util.AccessType; import org.hibernate.jpamodelgen.util.AccessTypeInformation; import org.hibernate.jpamodelgen.util.Constants; +import org.hibernate.jpamodelgen.util.NullnessUtil; import org.hibernate.jpamodelgen.util.TypeUtils; /** @@ -66,14 +67,19 @@ public class AnnotationMetaEntity implements MetaEntity { */ private MetaEntity entityToMerge; - public AnnotationMetaEntity(TypeElement element, Context context, boolean lazilyInitialised) { + public AnnotationMetaEntity(TypeElement element, Context context) { this.element = element; this.context = context; - this.members = new HashMap(); - this.importContext = new ImportContextImpl( getPackageName() ); + this.members = new HashMap<>(); + this.importContext = new ImportContextImpl( getPackageName( context, element ) ); + } + + public static AnnotationMetaEntity create(TypeElement element, Context context, boolean lazilyInitialised) { + final AnnotationMetaEntity annotationMetaEntity = new AnnotationMetaEntity( element, context ); if ( !lazilyInitialised ) { - init(); + annotationMetaEntity.init(); } + return annotationMetaEntity; } public AccessTypeInformation getEntityAccessTypeInfo() { @@ -93,6 +99,10 @@ public class AnnotationMetaEntity implements MetaEntity { } public final String getPackageName() { + return getPackageName( context, element ); + } + + private static String getPackageName(Context context, TypeElement element) { PackageElement packageOf = context.getElementUtils().getPackageOf( element ); return context.getElementUtils().getName( packageOf.getQualifiedName() ).toString(); } @@ -167,7 +177,8 @@ public class AnnotationMetaEntity implements MetaEntity { getContext().logMessage( Diagnostic.Kind.OTHER, "Initializing type " + getQualifiedName() + "." ); TypeUtils.determineAccessTypeForHierarchy( element, context ); - entityAccessTypeInfo = context.getAccessTypeInfo( getQualifiedName() ); + AccessTypeInformation accessTypeInfo = context.getAccessTypeInfo( getQualifiedName() ); + entityAccessTypeInfo = NullnessUtil.castNonNull(accessTypeInfo); List fieldsOfClass = ElementFilter.fieldsIn( element.getEnclosedElements() ); addPersistentMembers( fieldsOfClass, AccessType.FIELD ); diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/MetaAttributeGenerationVisitor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/MetaAttributeGenerationVisitor.java index d9f04df947..d2a04a6315 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/MetaAttributeGenerationVisitor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/MetaAttributeGenerationVisitor.java @@ -25,13 +25,16 @@ import org.hibernate.jpamodelgen.Context; import org.hibernate.jpamodelgen.util.AccessType; import org.hibernate.jpamodelgen.util.AccessTypeInformation; import org.hibernate.jpamodelgen.util.Constants; +import org.hibernate.jpamodelgen.util.NullnessUtil; import org.hibernate.jpamodelgen.util.StringUtil; import org.hibernate.jpamodelgen.util.TypeUtils; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Hardy Ferentschik */ -public class MetaAttributeGenerationVisitor extends SimpleTypeVisitor6 { +public class MetaAttributeGenerationVisitor extends SimpleTypeVisitor6<@Nullable AnnotationMetaAttribute, Element> { /** * FQCN of the Hibernate-specific {@code @Target} annotation. @@ -54,12 +57,12 @@ public class MetaAttributeGenerationVisitor extends SimpleTypeVisitor6 annotations) { + private @Nullable String getTargetEntity(List annotations) { String fullyQualifiedTargetEntityName = null; for ( AnnotationMirror mirror : annotations ) { if ( TypeUtils.isAnnotationMirrorOfType( mirror, Constants.ELEMENT_COLLECTION ) ) { @@ -266,7 +268,7 @@ public class MetaAttributeGenerationVisitor extends SimpleTypeVisitor6 { private final Context context; public BasicAttributeVisitor(Context context) { + super( Boolean.FALSE ); this.context = context; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/AccessTypeInformation.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/AccessTypeInformation.java index 6c89bd6e42..83987d5ee6 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/AccessTypeInformation.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/AccessTypeInformation.java @@ -6,6 +6,8 @@ */ package org.hibernate.jpamodelgen.util; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Encapsulates the access type information for a single class. * @@ -17,17 +19,17 @@ public class AccessTypeInformation { /** * Access type explicitly specified in xml or on an entity. */ - private AccessType explicitAccessType; + private @Nullable AccessType explicitAccessType; /** * The default type for en entity. This type might change during the parsing/discovering process. The reason * for that is the ability to mix and match xml and annotation configuration. */ - private AccessType defaultAccessType; + private @Nullable AccessType defaultAccessType; private static final AccessType DEFAULT_ACCESS_TYPE = AccessType.PROPERTY; - public AccessTypeInformation(String fqcn, AccessType explicitAccessType, AccessType defaultAccessType) { + public AccessTypeInformation(String fqcn, @Nullable AccessType explicitAccessType, @Nullable AccessType defaultAccessType) { this.fqcn = fqcn; this.explicitAccessType = explicitAccessType; this.defaultAccessType = defaultAccessType; @@ -58,7 +60,7 @@ public class AccessTypeInformation { this.explicitAccessType = explicitAccessType; } - public AccessType getDefaultAccessType() { + public @Nullable AccessType getDefaultAccessType() { return defaultAccessType; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/FileTimeStampChecker.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/FileTimeStampChecker.java index f10cfd9a33..6377f88b58 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/FileTimeStampChecker.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/FileTimeStampChecker.java @@ -10,6 +10,8 @@ import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Hardy Ferentschik */ @@ -26,7 +28,7 @@ public class FileTimeStampChecker implements Serializable { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if ( this == o ) { return true; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/NullnessUtil.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/NullnessUtil.java new file mode 100644 index 0000000000..d9ea22ec65 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/NullnessUtil.java @@ -0,0 +1,361 @@ +/* + * Checker Framework utilities + * Copyright 2004-present by the Checker Framework developers + * + * MIT License: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.hibernate.jpamodelgen.util; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; + +/** + * Utility class for the Nullness Checker. + * + *

To avoid the need to write the NullnessUtil class name, do: + * + *

import static org.checkerframework.checker.nullness.util.NullnessUtil.castNonNull;
+ *

+ * or + * + *

import static org.checkerframework.checker.nullness.util.NullnessUtil.*;
+ * + *

Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can copy this class into your own project. + */ +@SuppressWarnings({ + "nullness", // Nullness utilities are trusted regarding nullness. + "cast" // Casts look redundant if Nullness Checker is not run. +}) +@AnnotatedFor("nullness") +public final class NullnessUtil { + + private NullnessUtil() { + throw new AssertionError( "shouldn't be instantiated" ); + } + + /** + * A method that suppresses warnings from the Nullness Checker. + * + *

The method takes a possibly-null reference, unsafely casts it to have the @NonNull type + * qualifier, and returns it. The Nullness Checker considers both the return value, and also the + * argument, to be non-null after the method call. Therefore, the {@code castNonNull} method can + * be used either as a cast expression or as a statement. The Nullness Checker issues no warnings + * in any of the following code: + * + *


+	 *   // one way to use as a cast:
+	 *  {@literal @}NonNull String s = castNonNull(possiblyNull1);
+	 *
+	 *   // another way to use as a cast:
+	 *   castNonNull(possiblyNull2).toString();
+	 *
+	 *   // one way to use as a statement:
+	 *   castNonNull(possiblyNull3);
+	 *   possiblyNull3.toString();`
+	 * 
+ *

+ * The {@code castNonNull} method is intended to be used in situations where the programmer + * definitively knows that a given reference is not null, but the type system is unable to make + * this deduction. It is not intended for defensive programming, in which a programmer cannot + * prove that the value is not null but wishes to have an earlier indication if it is. See the + * Checker Framework Manual for further discussion. + * + *

The method throws {@link AssertionError} if Java assertions are enabled and the argument is + * {@code null}. If the exception is ever thrown, then that indicates that the programmer misused + * the method by using it in a circumstance where its argument can be null. + * + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time + * + * @return the argument, casted to have the type qualifier @NonNull + */ + @EnsuresNonNull("#1") + public static @NonNull T castNonNull(@Nullable T ref) { + assert ref != null : "Misuse of castNonNull: called with a null argument"; + return (@NonNull T) ref; + } + + /** + * Suppress warnings from the Nullness Checker, with a custom error message. See {@link + * #castNonNull(Object)} for documentation. + * + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull + * + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") @NonNull T castNonNull( + @Nullable T ref, String message) { + assert ref != null : "Misuse of castNonNull: called with a null argument: " + message; + return (@NonNull T) ref; + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [] castNonNullDeep( + T @Nullable [] arr) { + return (@NonNull T[]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [] castNonNullDeep( + T @Nullable [] arr, String message) { + return (@NonNull T[]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][] castNonNullDeep( + T @Nullable [] @Nullable [] arr) { + return (@NonNull T[][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][] castNonNullDeep( + T @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][][]) castNonNullArray( arr, message ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { + return (@NonNull T[][][][][]) castNonNullArray( arr, null ); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that all + * elements at every array level are non-null. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if this method is misused + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + * + * @see #castNonNull(Object) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][][][]) castNonNullArray( arr, message ); + } + + /** + * The implementation of castNonNullDeep. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at run + * time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + * + * @return the argument, casted to have the type qualifier @NonNull at all levels + */ + private static @NonNull T @NonNull [] castNonNullArray( + T @Nullable [] arr, @Nullable String message) { + assert arr != null + : "Misuse of castNonNullArray: called with a null array argument" + + ( ( message == null ) ? "" : ( ": " + message ) ); + for ( int i = 0; i < arr.length; ++i ) { + assert arr[i] != null + : "Misuse of castNonNull: called with a null array element" + + ( ( message == null ) ? "" : ( ": " + message ) ); + checkIfArray( arr[i], message ); + } + return (@NonNull T[]) arr; + } + + /** + * If the argument is an array, requires it to be non-null at all levels. + * + * @param ref a value; if an array, all of its elements, and their elements recursively, are + * non-null at run time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + */ + private static void checkIfArray(@NonNull Object ref, @Nullable String message) { + assert ref != null + : "Misuse of checkIfArray: called with a null argument" + + ( ( message == null ) ? "" : ( ": " + message ) ); + Class comp = ref.getClass().getComponentType(); + if ( comp != null ) { + // comp is non-null for arrays, otherwise null. + if ( comp.isPrimitive() ) { + // Nothing to do for arrays of primitive type: primitives are + // never null. + } + else { + castNonNullArray( (Object[]) ref, message ); + } + } + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/TypeRenderingVisitor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/TypeRenderingVisitor.java index 8654c95d84..632dcc4fe9 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/TypeRenderingVisitor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/TypeRenderingVisitor.java @@ -26,10 +26,12 @@ import javax.lang.model.type.UnionType; import javax.lang.model.type.WildcardType; import javax.lang.model.util.SimpleTypeVisitor8; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Christian Beikov */ -public final class TypeRenderingVisitor extends SimpleTypeVisitor8 { +public final class TypeRenderingVisitor extends SimpleTypeVisitor8<@Nullable Object, @Nullable Object> { private final StringBuilder sb = new StringBuilder(); private final Set visitedTypeVariables = new HashSet<>(); @@ -66,7 +68,7 @@ public final class TypeRenderingVisitor extends SimpleTypeVisitor8 typeArguments = t.getTypeArguments(); if ( !typeArguments.isEmpty() ) { @@ -127,7 +129,7 @@ public final class TypeRenderingVisitor extends SimpleTypeVisitor8 bounds = t.getBounds(); bounds.get( 0 ).accept( this, null ); for ( int i = 0; i < bounds.size(); i++ ) { @@ -175,12 +177,12 @@ public final class TypeRenderingVisitor extends SimpleTypeVisitor8 myMembers = searchedElement.getEnclosedElements(); for ( Element subElement : myMembers ) { List entityAnnotations = @@ -385,7 +387,7 @@ public final class TypeUtils { return null; } - private static AccessType getAccessTypeOfIdAnnotation(Element element) { + private static @Nullable AccessType getAccessTypeOfIdAnnotation(Element element) { AccessType accessType = null; final ElementKind kind = element.getKind(); if ( kind == ElementKind.FIELD || kind == ElementKind.METHOD ) { @@ -399,13 +401,12 @@ public final class TypeUtils { || TypeUtils.isAnnotationMirrorOfType( annotationMirror, Constants.EMBEDDED_ID ); } - public static AccessType determineAnnotationSpecifiedAccessType(Element element) { + public static @Nullable AccessType determineAnnotationSpecifiedAccessType(Element element) { final AnnotationMirror accessAnnotationMirror = TypeUtils.getAnnotationMirror( element, Constants.ACCESS ); AccessType forcedAccessType = null; if ( accessAnnotationMirror != null ) { - Element accessElement = (Element) TypeUtils.getAnnotationValue( - accessAnnotationMirror, - DEFAULT_ANNOTATION_PARAMETER_NAME + Element accessElement = (Element) NullnessUtil.castNonNull( + TypeUtils.getAnnotationValue( accessAnnotationMirror, DEFAULT_ANNOTATION_PARAMETER_NAME ) ); if ( accessElement.getKind().equals( ElementKind.ENUM_CONSTANT ) ) { if ( accessElement.getSimpleName().toString().equals( AccessType.PROPERTY.toString() ) ) { @@ -436,7 +437,7 @@ public final class TypeUtils { return extractClosestRealTypeAsString( typeArguments.get( 0 ), context ); } - static class EmbeddedAttributeVisitor extends SimpleTypeVisitor6 { + static class EmbeddedAttributeVisitor extends SimpleTypeVisitor6<@Nullable String, Element> { private Context context; EmbeddedAttributeVisitor(Context context) { @@ -444,7 +445,7 @@ public final class TypeUtils { } @Override - public String visitDeclared(DeclaredType declaredType, Element element) { + public @Nullable String visitDeclared(DeclaredType declaredType, Element element) { TypeElement returnedElement = (TypeElement) context.getTypeUtils().asElement( declaredType ); String fqNameOfReturnType = null; if ( containsAnnotation( returnedElement, Constants.EMBEDDABLE ) ) { @@ -454,7 +455,7 @@ public final class TypeUtils { } @Override - public String visitExecutable(ExecutableType t, Element p) { + public @Nullable String visitExecutable(ExecutableType t, Element p) { if ( !p.getKind().equals( ElementKind.METHOD ) ) { return null; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/JpaNamespaceTransformingEventReader.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/JpaNamespaceTransformingEventReader.java index 763adb0ddf..9154e13a12 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/JpaNamespaceTransformingEventReader.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/JpaNamespaceTransformingEventReader.java @@ -131,10 +131,10 @@ public class JpaNamespaceTransformingEventReader extends EventReaderDelegate { } private List updateElementNamespaces(StartElement startElement) { - List newNamespaceList = new ArrayList(); - Iterator existingNamespaceIterator = startElement.getNamespaces(); + List newNamespaceList = new ArrayList<>(); + Iterator existingNamespaceIterator = startElement.getNamespaces(); while ( existingNamespaceIterator.hasNext() ) { - Namespace namespace = (Namespace) existingNamespaceIterator.next(); + Namespace namespace = existingNamespaceIterator.next(); if ( NAMESPACE_MAPPING.containsKey( namespace.getNamespaceURI() ) ) { newNamespaceList.add( xmlEventFactory.createNamespace( EMPTY_PREFIX, currentDocumentNamespaceUri ) ); } @@ -153,10 +153,10 @@ public class JpaNamespaceTransformingEventReader extends EventReaderDelegate { private List updateElementAttributes(StartElement startElement) { // adjust the version attribute - List newElementAttributeList = new ArrayList(); - Iterator existingAttributesIterator = startElement.getAttributes(); + List newElementAttributeList = new ArrayList<>(); + Iterator existingAttributesIterator = startElement.getAttributes(); while ( existingAttributesIterator.hasNext() ) { - Attribute attribute = (Attribute) existingAttributesIterator.next(); + Attribute attribute = existingAttributesIterator.next(); if ( VERSION_ATTRIBUTE_NAME.equals( attribute.getName().getLocalPart() ) ) { if ( currentDocumentNamespaceUri.equals( DEFAULT_PERSISTENCE_NAMESPACE ) ) { if ( !DEFAULT_PERSISTENCE_VERSION.equals( attribute.getName().getPrefix() ) ) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/XmlParserHelper.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/XmlParserHelper.java index d22222a88a..dec3d83e9a 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/XmlParserHelper.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/util/xml/XmlParserHelper.java @@ -24,8 +24,10 @@ import javax.xml.validation.SchemaFactory; import org.hibernate.jpamodelgen.Context; import org.hibernate.jpamodelgen.util.Constants; +import org.hibernate.jpamodelgen.util.NullnessUtil; import org.hibernate.jpamodelgen.xml.jaxb.ObjectFactory; +import org.checkerframework.checker.nullness.qual.Nullable; import org.xml.sax.SAXException; /** @@ -48,7 +50,7 @@ public class XmlParserHelper { private static final XMLInputFactory XML_INPUT_FACTORY = XMLInputFactory.newInstance(); - private static final ConcurrentMap SCHEMA_CACHE = new ConcurrentHashMap( + private static final ConcurrentMap SCHEMA_CACHE = new ConcurrentHashMap<>( NUMBER_OF_SCHEMAS ); @@ -66,7 +68,7 @@ public class XmlParserHelper { * * @return an input stream for the specified resource or {@code null} in case resource cannot be loaded */ - public InputStream getInputStreamForResource(String resource) { + public @Nullable InputStream getInputStreamForResource(String resource) { // METAGEN-75 if ( !resource.startsWith( RESOURCE_PATH_SEPARATOR ) ) { resource = RESOURCE_PATH_SEPARATOR + resource; @@ -160,19 +162,17 @@ public class XmlParserHelper { } private Schema loadSchema(String schemaName) throws XmlParsingException { - Schema schema = null; - URL schemaUrl = this.getClass().getClassLoader().getResource( schemaName ); + URL schemaUrl = NullnessUtil.castNonNull( this.getClass().getClassLoader() ).getResource( schemaName ); if ( schemaUrl == null ) { - return schema; + throw new IllegalArgumentException( "Couldn't find schema on classpath: " + schemaName ); } SchemaFactory sf = SchemaFactory.newInstance( javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI ); try { - schema = sf.newSchema( schemaUrl ); + return sf.newSchema( schemaUrl ); } catch ( SAXException e ) { throw new XmlParsingException( "Unable to create schema for " + schemaName + ": " + e.getMessage(), e ); } - return schema; } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/JpaDescriptorParser.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/JpaDescriptorParser.java index 92ef92b30f..0576571e1e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/JpaDescriptorParser.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/JpaDescriptorParser.java @@ -38,6 +38,8 @@ import org.hibernate.jpamodelgen.xml.jaxb.Persistence; import org.hibernate.jpamodelgen.xml.jaxb.PersistenceUnitDefaults; import org.hibernate.jpamodelgen.xml.jaxb.PersistenceUnitMetadata; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Parser for JPA XML descriptors (persistence.xml and referenced mapping files). * @@ -105,7 +107,7 @@ public class JpaDescriptorParser { return mappingFileNames; } - private Persistence getPersistence() { + private @Nullable Persistence getPersistence() { Persistence persistence = null; String persistenceXmlLocation = context.getPersistenceXmlLocation(); final InputStream stream = xmlParserHelper.getInputStreamForResource( persistenceXmlLocation ); @@ -249,7 +251,7 @@ public class JpaDescriptorParser { continue; } - XmlMetaEntity metaEntity = new XmlMetaEntity( + XmlMetaEntity metaEntity = XmlMetaEntity.create( entity, defaultPackageName, getXmlMappedType( fqcn ), context ); if ( context.containsMetaEntity( fqcn ) ) { @@ -333,8 +335,9 @@ public class JpaDescriptorParser { private AccessType determineEntityAccessType(EntityMappings mappings) { AccessType accessType = context.getPersistenceUnitDefaultAccessType(); - if ( mappings.getAccess() != null ) { - accessType = mapXmlAccessTypeToJpaAccessType( mappings.getAccess() ); + final org.hibernate.jpamodelgen.xml.jaxb.AccessType mappingsAccess = mappings.getAccess(); + if ( mappingsAccess != null ) { + accessType = mapXmlAccessTypeToJpaAccessType( mappingsAccess ); } return accessType; } @@ -456,15 +459,11 @@ public class JpaDescriptorParser { private AccessType mapXmlAccessTypeToJpaAccessType(org.hibernate.jpamodelgen.xml.jaxb.AccessType xmlAccessType) { switch ( xmlAccessType ) { - case FIELD: { + case FIELD: return AccessType.FIELD; - } - case PROPERTY: { + case PROPERTY: return AccessType.PROPERTY; - } - default: { - } } - return null; + throw new IllegalArgumentException( "Unknown access type: " + xmlAccessType ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/XmlMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/XmlMetaEntity.java index 18466bac1b..2ffcaf7053 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/XmlMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/xml/XmlMetaEntity.java @@ -26,6 +26,7 @@ import org.hibernate.jpamodelgen.model.ImportContext; import org.hibernate.jpamodelgen.model.MetaAttribute; import org.hibernate.jpamodelgen.model.MetaEntity; import org.hibernate.jpamodelgen.util.AccessTypeInformation; +import org.hibernate.jpamodelgen.util.NullnessUtil; import org.hibernate.jpamodelgen.util.StringUtil; import org.hibernate.jpamodelgen.util.TypeUtils; import org.hibernate.jpamodelgen.xml.jaxb.Attributes; @@ -44,6 +45,8 @@ import org.hibernate.jpamodelgen.xml.jaxb.MappedSuperclass; import org.hibernate.jpamodelgen.xml.jaxb.OneToMany; import org.hibernate.jpamodelgen.xml.jaxb.OneToOne; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Collects XML-based meta information about an annotated type (entity, embeddable or mapped superclass). * @@ -64,13 +67,13 @@ public class XmlMetaEntity implements MetaEntity { private final String packageName; private final String defaultPackageName; private final ImportContext importContext; - private final List members = new ArrayList(); + private final List members = new ArrayList<>(); private final TypeElement element; private final Context context; private final boolean isMetaComplete; - private Attributes attributes; - private EmbeddableAttributes embeddableAttributes; + private @Nullable Attributes attributes; + private @Nullable EmbeddableAttributes embeddableAttributes; private AccessTypeInformation accessTypeInfo; /** @@ -90,8 +93,13 @@ public class XmlMetaEntity implements MetaEntity { this( ormEntity.getClazz(), defaultPackageName, element, context, ormEntity.isMetadataComplete() ); this.attributes = ormEntity.getAttributes(); this.embeddableAttributes = null; + } + + static XmlMetaEntity create(Entity ormEntity, String defaultPackageName, TypeElement element, Context context) { + XmlMetaEntity entity = new XmlMetaEntity( ormEntity, defaultPackageName, element, context ); // entities can be directly initialised - init(); + entity.init(); + return entity; } XmlMetaEntity(MappedSuperclass mappedSuperclass, String defaultPackageName, TypeElement element, Context context) { @@ -125,15 +133,15 @@ public class XmlMetaEntity implements MetaEntity { this.clazzName = className; this.packageName = pkg; this.context = context; - this.importContext = new ImportContextImpl( getPackageName() ); + this.importContext = new ImportContextImpl( pkg ); this.element = element; - this.isMetaComplete = initIsMetaComplete( metaComplete ); + this.isMetaComplete = initIsMetaComplete( context, metaComplete ); } private final void init() { context.logMessage( Diagnostic.Kind.OTHER, "Initializing type " + getQualifiedName() + "." ); - this.accessTypeInfo = context.getAccessTypeInfo( getQualifiedName() ); + this.accessTypeInfo = NullnessUtil.castNonNull( context.getAccessTypeInfo( getQualifiedName() ) ); if ( attributes != null ) { parseAttributes( attributes ); } @@ -197,11 +205,11 @@ public class XmlMetaEntity implements MetaEntity { return sb.toString(); } - private boolean initIsMetaComplete(Boolean metadataComplete) { + private static boolean initIsMetaComplete(Context context, Boolean metadataComplete) { return context.isFullyXmlConfigured() || Boolean.TRUE.equals( metadataComplete ); } - private String[] getCollectionTypes(String propertyName, String explicitTargetEntity, String explicitMapKeyClass, ElementKind expectedElementKind) { + private @Nullable String @Nullable[] getCollectionTypes(String propertyName, String explicitTargetEntity, @Nullable String explicitMapKeyClass, ElementKind expectedElementKind) { for ( Element elem : element.getEnclosedElements() ) { if ( !expectedElementKind.equals( elem.getKind() ) ) { continue; @@ -226,7 +234,7 @@ public class XmlMetaEntity implements MetaEntity { return null; } - private DeclaredType determineDeclaredType(Element elem) { + private @Nullable DeclaredType determineDeclaredType(Element elem) { DeclaredType type = null; if ( elem.asType() instanceof DeclaredType ) { type = ( (DeclaredType) elem.asType() ); @@ -240,17 +248,16 @@ public class XmlMetaEntity implements MetaEntity { return type; } - private String[] determineTypes(String propertyName, String explicitTargetEntity, String explicitMapKeyClass, DeclaredType type) { - String[] types = new String[3]; + private @Nullable String[] determineTypes(String propertyName, String explicitTargetEntity, @Nullable String explicitMapKeyClass, DeclaredType type) { + @Nullable String[] types = new String[3]; determineTargetType( type, propertyName, explicitTargetEntity, types ); - determineCollectionType( type, types ); - if ( types[1].equals( "jakarta.persistence.metamodel.MapAttribute" ) ) { + if ( determineCollectionType( type, types ).equals( "jakarta.persistence.metamodel.MapAttribute" ) ) { determineMapType( type, explicitMapKeyClass, types ); } return types; } - private void determineMapType(DeclaredType type, String explicitMapKeyClass, String[] types) { + private void determineMapType(DeclaredType type, @Nullable String explicitMapKeyClass, @Nullable String[] types) { if ( explicitMapKeyClass != null ) { types[2] = explicitMapKeyClass; } @@ -259,11 +266,11 @@ public class XmlMetaEntity implements MetaEntity { } } - private void determineCollectionType(DeclaredType type, String[] types) { - types[1] = COLLECTIONS.get( type.asElement().toString() ); + private String determineCollectionType(DeclaredType type, @Nullable String[] types) { + return NullnessUtil.castNonNull( types[1] = COLLECTIONS.get( type.asElement().toString() ) ); } - private void determineTargetType(DeclaredType type, String propertyName, String explicitTargetEntity, String[] types) { + private void determineTargetType(DeclaredType type, String propertyName, String explicitTargetEntity, @Nullable String[] types) { List typeArguments = type.getTypeArguments(); if ( typeArguments.size() == 0 && explicitTargetEntity == null ) { @@ -288,7 +295,7 @@ public class XmlMetaEntity implements MetaEntity { * @return The entity type for this property or {@code null} if the property with the name and the matching access * type does not exist. */ - private String getType(String propertyName, String explicitTargetEntity, ElementKind expectedElementKind) { + private @Nullable String getType(String propertyName, @Nullable String explicitTargetEntity, ElementKind expectedElementKind) { for ( Element elem : element.getEnclosedElements() ) { if ( !expectedElementKind.equals( elem.getKind() ) ) { continue; @@ -413,7 +420,7 @@ public class XmlMetaEntity implements MetaEntity { } } - private void parseEmbeddableAttributes(EmbeddableAttributes attributes) { + private void parseEmbeddableAttributes(@Nullable EmbeddableAttributes attributes) { if ( attributes == null ) { return; } @@ -449,7 +456,7 @@ public class XmlMetaEntity implements MetaEntity { } private boolean parseElementCollection(ElementCollection collection) { - String[] types; + @Nullable String @Nullable[] types; XmlMetaCollection metaCollection; ElementKind elementKind = getElementKind( collection.getAccess() ); String explicitTargetClass = determineExplicitTargetEntity( collection.getTargetClass() ); @@ -464,11 +471,14 @@ public class XmlMetaEntity implements MetaEntity { return true; } if ( types != null ) { - if ( types[2] == null ) { - metaCollection = new XmlMetaCollection( this, collection.getName(), types[0], types[1] ); + final String type = NullnessUtil.castNonNull( types[0] ); + final String collectionType = NullnessUtil.castNonNull( types[1] ); + final String keyType = types[2]; + if ( keyType == null ) { + metaCollection = new XmlMetaCollection( this, collection.getName(), type, collectionType ); } else { - metaCollection = new XmlMetaMap( this, collection.getName(), types[0], types[1], types[2] ); + metaCollection = new XmlMetaMap( this, collection.getName(), type, collectionType, keyType ); } members.add( metaCollection ); } @@ -495,7 +505,7 @@ public class XmlMetaEntity implements MetaEntity { return explicitTargetClass; } - private String determineExplicitMapKeyClass(MapKeyClass mapKeyClass) { + private @Nullable String determineExplicitMapKeyClass(MapKeyClass mapKeyClass) { String explicitMapKey = null; if ( mapKeyClass != null ) { explicitMapKey = StringUtil.determineFullyQualifiedClassName( defaultPackageName, mapKeyClass.getClazz() ); @@ -504,7 +514,7 @@ public class XmlMetaEntity implements MetaEntity { } private boolean parseOneToMany(OneToMany oneToMany) { - String[] types; + @Nullable String @Nullable [] types; XmlMetaCollection metaCollection; ElementKind elementKind = getElementKind( oneToMany.getAccess() ); String explicitTargetClass = determineExplicitTargetEntity( oneToMany.getTargetEntity() ); @@ -517,11 +527,14 @@ public class XmlMetaEntity implements MetaEntity { return true; } if ( types != null ) { - if ( types[2] == null ) { - metaCollection = new XmlMetaCollection( this, oneToMany.getName(), types[0], types[1] ); + final String type = NullnessUtil.castNonNull( types[0] ); + final String collectionType = NullnessUtil.castNonNull( types[1] ); + final String keyType = types[2]; + if ( keyType == null ) { + metaCollection = new XmlMetaCollection( this, oneToMany.getName(), type, collectionType ); } else { - metaCollection = new XmlMetaMap( this, oneToMany.getName(), types[0], types[1], types[2] ); + metaCollection = new XmlMetaMap( this, oneToMany.getName(), type, collectionType, keyType ); } members.add( metaCollection ); } @@ -529,7 +542,7 @@ public class XmlMetaEntity implements MetaEntity { } private boolean parseManyToMany(ManyToMany manyToMany) { - String[] types; + @Nullable String @Nullable [] types; XmlMetaCollection metaCollection; ElementKind elementKind = getElementKind( manyToMany.getAccess() ); String explicitTargetClass = determineExplicitTargetEntity( manyToMany.getTargetEntity() ); @@ -544,11 +557,14 @@ public class XmlMetaEntity implements MetaEntity { return true; } if ( types != null ) { - if ( types[2] == null ) { - metaCollection = new XmlMetaCollection( this, manyToMany.getName(), types[0], types[1] ); + final String type = NullnessUtil.castNonNull( types[0] ); + final String collectionType = NullnessUtil.castNonNull( types[1] ); + final String keyType = types[2]; + if ( keyType == null ) { + metaCollection = new XmlMetaCollection( this, manyToMany.getName(), type, collectionType ); } else { - metaCollection = new XmlMetaMap( this, manyToMany.getName(), types[0], types[1], types[2] ); + metaCollection = new XmlMetaMap( this, manyToMany.getName(), type, collectionType, keyType ); } members.add( metaCollection ); }