HHH-17117 allow @TenantId to form part of composite key

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-08-26 16:15:59 +02:00
parent 77a34e6312
commit d90807f9e4
14 changed files with 251 additions and 86 deletions

View File

@ -26,6 +26,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* @author Gavin King * @author Gavin King
*/ */
@ValueGenerationType(generatedBy = TenantIdGeneration.class) @ValueGenerationType(generatedBy = TenantIdGeneration.class)
@IdGeneratorType(TenantIdGeneration.class)
@AttributeBinderType(binder = TenantIdBinder.class) @AttributeBinderType(binder = TenantIdBinder.class)
@Target({METHOD, FIELD}) @Target({METHOD, FIELD})
@Retention(RUNTIME) @Retention(RUNTIME)

View File

@ -1358,24 +1358,6 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
} }
} }
private static AnnotatedClassType getAnnotatedClassType2(ClassDetails classDetails) {
if ( classDetails.hasDirectAnnotationUsage( Entity.class ) ) {
return AnnotatedClassType.ENTITY;
}
else if ( classDetails.hasDirectAnnotationUsage( Embeddable.class ) ) {
return AnnotatedClassType.EMBEDDABLE;
}
else if ( classDetails.hasDirectAnnotationUsage( jakarta.persistence.MappedSuperclass.class ) ) {
return AnnotatedClassType.MAPPED_SUPERCLASS;
}
else if ( classDetails.hasDirectAnnotationUsage( Imported.class ) ) {
return AnnotatedClassType.IMPORTED;
}
else {
return AnnotatedClassType.NONE;
}
}
@Override @Override
public void addMappedSuperclass(Class<?> type, MappedSuperclass mappedSuperclass) { public void addMappedSuperclass(Class<?> type, MappedSuperclass mappedSuperclass) {
if ( mappedSuperClasses == null ) { if ( mappedSuperClasses == null ) {
@ -2022,10 +2004,10 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
} }
} }
} }
stopProcess = failingSecondPasses.size() == 0 || failingSecondPasses.size() == endOfQueueFkSecondPasses.size(); stopProcess = failingSecondPasses.isEmpty() || failingSecondPasses.size() == endOfQueueFkSecondPasses.size();
endOfQueueFkSecondPasses = failingSecondPasses; endOfQueueFkSecondPasses = failingSecondPasses;
} }
if ( endOfQueueFkSecondPasses.size() > 0 ) { if ( !endOfQueueFkSecondPasses.isEmpty() ) {
throw originalException; throw originalException;
} }
} }
@ -2212,7 +2194,8 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
handleIdentifierValueBinding( handleIdentifierValueBinding(
entityBinding.getIdentifier(), entityBinding.getIdentifier(),
dialect, dialect,
(RootClass) entityBinding (RootClass) entityBinding,
entityBinding.getIdentifierProperty()
); );
} }
@ -2225,22 +2208,20 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
handleIdentifierValueBinding( handleIdentifierValueBinding(
( (IdentifierCollection) collection ).getIdentifier(), ( (IdentifierCollection) collection ).getIdentifier(),
dialect, dialect,
null,
null null
); );
} }
} }
private void handleIdentifierValueBinding( private void handleIdentifierValueBinding(
KeyValue identifierValueBinding, KeyValue identifierValueBinding, Dialect dialect, RootClass entityBinding, Property identifierProperty) {
Dialect dialect,
RootClass entityBinding) {
// todo : store this result (back into the entity or into the KeyValue, maybe?) // todo : store this result (back into the entity or into the KeyValue, maybe?)
// This process of instantiating the id-generator is called multiple times. // This process of instantiating the id-generator is called multiple times.
// It was done this way in the old code too, so no "regression" here; but // It was done this way in the old code too, so no "regression" here; but
// it could be done better // it could be done better
try { try {
final Generator generator = identifierValueBinding.createGenerator( dialect, entityBinding ); final Generator generator = identifierValueBinding.createGenerator( dialect, entityBinding, identifierProperty );
if ( generator instanceof ExportableProducer ) { if ( generator instanceof ExportableProducer ) {
( (ExportableProducer) generator ).registerExportables( getDatabase() ); ( (ExportableProducer) generator ).registerExportables( getDatabase() );
} }

View File

@ -25,7 +25,6 @@ import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.Status; import org.hibernate.engine.spi.Status;
import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.EventSource;
import org.hibernate.id.Assigned;
import org.hibernate.id.CompositeNestedGeneratedValueGenerator; import org.hibernate.id.CompositeNestedGeneratedValueGenerator;
import org.hibernate.id.IdentifierGenerationException; import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreLogging;

View File

@ -51,24 +51,26 @@ public class TenantIdGeneration implements BeforeExecutionGenerator {
@Override @Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) { public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
final JavaType<Object> tenantIdentifierJavaType = sessionFactory.getTenantIdentifierJavaType();
final Object tenantId = session.getTenantIdentifierValue(); final Object tenantId = session.getTenantIdentifierValue();
if ( currentValue != null ) { if ( currentValue != null ) {
final CurrentTenantIdentifierResolver<Object> resolver = sessionFactory.getCurrentTenantIdentifierResolver(); final CurrentTenantIdentifierResolver<Object> resolver =
sessionFactory.getCurrentTenantIdentifierResolver();
if ( resolver != null && resolver.isRoot( tenantId ) ) { if ( resolver != null && resolver.isRoot( tenantId ) ) {
// the "root" tenant is allowed to set the tenant id explicitly // the "root" tenant is allowed to set the tenant id explicitly
return currentValue; return currentValue;
} }
if ( !tenantIdentifierJavaType.areEqual( currentValue, tenantId ) ) { else {
throw new PropertyValueException( final JavaType<Object> tenantIdJavaType = sessionFactory.getTenantIdentifierJavaType();
"assigned tenant id differs from current tenant id: " + if ( !tenantIdJavaType.areEqual( currentValue, tenantId ) ) {
tenantIdentifierJavaType.toString( currentValue ) + throw new PropertyValueException(
"!=" + "assigned tenant id differs from current tenant id ["
tenantIdentifierJavaType.toString( tenantId ), + tenantIdJavaType.toString( currentValue )
entityName, + " != "
propertyName + tenantIdJavaType.toString( tenantId ) + "]",
); entityName,
propertyName
);
}
} }
} }
return tenantId; return tenantId;

View File

@ -103,15 +103,16 @@ public class FilterImpl implements Filter, Serializable {
* of the passed value did not match the configured type. * of the passed value did not match the configured type.
*/ */
public Filter setParameter(String name, Object value) throws IllegalArgumentException { public Filter setParameter(String name, Object value) throws IllegalArgumentException {
Object argument = definition.processArgument(value); final Object argument = definition.processArgument( value );
// Make sure this is a defined parameter and check the incoming value type // Make sure this is a defined parameter and check the incoming value type
JdbcMapping type = definition.getParameterJdbcMapping( name ); final JdbcMapping type = definition.getParameterJdbcMapping( name );
if ( type == null ) { if ( type == null ) {
throw new IllegalArgumentException( "Undefined filter parameter [" + name + "]" ); throw new IllegalArgumentException( "Undefined filter parameter '" + name + "'" );
} }
if ( argument != null && !type.getJavaTypeDescriptor().isInstance( argument ) ) { if ( argument != null && !type.getJavaTypeDescriptor().isInstance( argument ) ) {
throw new IllegalArgumentException( "Incorrect type for parameter [" + name + "]" ); throw new IllegalArgumentException( "Argument assigned to filter parameter '" + name
+ "' is not of type '" + type.getJavaTypeDescriptor().getTypeName() + "'" );
} }
if ( parameters == null ) { if ( parameters == null ) {
parameters = new TreeMap<>(); parameters = new TreeMap<>();
@ -133,14 +134,15 @@ public class FilterImpl implements Filter, Serializable {
if ( values == null ) { if ( values == null ) {
throw new IllegalArgumentException( "Collection must be not null" ); throw new IllegalArgumentException( "Collection must be not null" );
} }
JdbcMapping type = definition.getParameterJdbcMapping( name ); final JdbcMapping type = definition.getParameterJdbcMapping( name );
if ( type == null ) { if ( type == null ) {
throw new HibernateException( "Undefined filter parameter [" + name + "]" ); throw new HibernateException( "Undefined filter parameter '" + name + "'" );
} }
if ( !values.isEmpty() ) { if ( !values.isEmpty() ) {
final Object element = values.iterator().next(); final Object element = values.iterator().next();
if ( !type.getJavaTypeDescriptor().isInstance( element ) ) { if ( !type.getJavaTypeDescriptor().isInstance( element ) ) {
throw new HibernateException( "Incorrect type for parameter [" + name + "]" ); throw new IllegalArgumentException( "Argument assigned to filter parameter '" + name
+ "' is not of type '" + type.getJavaTypeDescriptor().getTypeName() + "'" );
} }
} }
if ( parameters == null ) { if ( parameters == null ) {

View File

@ -457,7 +457,7 @@ public class SessionFactoryImpl extends QueryParameterBindingTypeResolverImpl im
for ( PersistentClass model : bootMetamodel.getEntityBindings() ) { for ( PersistentClass model : bootMetamodel.getEntityBindings() ) {
if ( !model.isInherited() ) { if ( !model.isInherited() ) {
final KeyValue id = model.getIdentifier(); final KeyValue id = model.getIdentifier();
final Generator generator = id.createGenerator( dialect, (RootClass) model ); final Generator generator = id.createGenerator( dialect, (RootClass) model, model.getIdentifierProperty() );
if ( generator instanceof Configurable ) { if ( generator instanceof Configurable ) {
final Configurable identifierGenerator = (Configurable) generator; final Configurable identifierGenerator = (Configurable) generator;
identifierGenerator.initialize( sqlStringGenerationContext ); identifierGenerator.initialize( sqlStringGenerationContext );

View File

@ -11,7 +11,6 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -546,10 +545,6 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
this.isKey = isKey; this.isKey = isKey;
} }
public boolean hasPojoRepresentation() {
return componentClassName!=null;
}
/** /**
* Returns the {@link Property} at the specified position in this {@link Component}. * Returns the {@link Property} at the specified position in this {@link Component}.
* *
@ -661,37 +656,33 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
} }
@Override @Override
public Generator createGenerator(Dialect dialect, RootClass rootClass) throws MappingException { public Generator createGenerator(Dialect dialect, RootClass rootClass, Property property) {
if ( builtIdentifierGenerator == null ) { if ( builtIdentifierGenerator == null ) {
builtIdentifierGenerator = buildIdentifierGenerator( dialect, rootClass ); builtIdentifierGenerator =
getCustomIdGeneratorCreator().isAssigned()
? buildIdentifierGenerator( dialect, rootClass )
: super.createGenerator( dialect, rootClass, property );
} }
return builtIdentifierGenerator; return builtIdentifierGenerator;
} }
private Generator buildIdentifierGenerator( Dialect dialect, RootClass rootClass) throws MappingException { private Generator buildIdentifierGenerator(Dialect dialect, RootClass rootClass) {
if ( !getCustomIdGeneratorCreator().isAssigned() ) {
return super.createGenerator( dialect, rootClass );
}
final Class<?> entityClass = rootClass.getMappedClass();
final Class<?> attributeDeclarer = getAttributeDeclarer( rootClass, entityClass );
final CompositeNestedGeneratedValueGenerator.GenerationContextLocator locator =
new StandardGenerationContextLocator( rootClass.getEntityName() );
final CompositeNestedGeneratedValueGenerator generator = final CompositeNestedGeneratedValueGenerator generator =
new CompositeNestedGeneratedValueGenerator( locator, getType() ); new CompositeNestedGeneratedValueGenerator(
new StandardGenerationContextLocator( rootClass.getEntityName() ),
getType()
);
final List<Property> properties = getProperties(); final List<Property> properties = getProperties();
for ( int i = 0; i < properties.size(); i++ ) { for ( int i = 0; i < properties.size(); i++ ) {
final Property property = properties.get( i ); final Property property = properties.get( i );
if ( property.getValue().isSimpleValue() ) { if ( property.getValue().isSimpleValue() ) {
final SimpleValue value = (SimpleValue) property.getValue(); final SimpleValue value = (SimpleValue) property.getValue();
if ( !value.getCustomIdGeneratorCreator().isAssigned() ) { if ( !value.getCustomIdGeneratorCreator().isAssigned() ) {
// skip any 'assigned' generators, they would have been handled by // skip any 'assigned' generators, they would have been
// the StandardGenerationContextLocator // handled by the StandardGenerationContextLocator
generator.addGeneratedValuePlan( new ValueGenerationPlan( generator.addGeneratedValuePlan( new ValueGenerationPlan(
value.createGenerator( dialect, rootClass ), value.createGenerator( dialect, rootClass, property ),
getType().isMutable() ? injector( property, attributeDeclarer ) : null, getType().isMutable() ? injector( property, getAttributeDeclarer( rootClass ) ) : null,
i i
) ); ) );
} }
@ -700,23 +691,27 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable
return generator; return generator;
} }
private Class<?> getAttributeDeclarer(RootClass rootClass, Class<?> entityClass) { /**
final Class<?> attributeDeclarer; // what class is the declarer of the composite pk attributes * Return the class that declares the composite pk attributes,
// IMPL NOTE : See the javadoc discussion on CompositeNestedGeneratedValueGenerator wrt the * which might be an {@code @IdClass}, an {@code @EmbeddedId},
// various scenarios for which we need to account here * of the entity class itself.
*/
private Class<?> getAttributeDeclarer(RootClass rootClass) {
// See the javadoc discussion on CompositeNestedGeneratedValueGenerator
// for the various scenarios we need to account for here
if ( rootClass.getIdentifierMapper() != null ) { if ( rootClass.getIdentifierMapper() != null ) {
// we have the @IdClass / <composite-id mapped="true"/> case // we have the @IdClass / <composite-id mapped="true"/> case
attributeDeclarer = resolveComponentClass(); return resolveComponentClass();
} }
else if ( rootClass.getIdentifierProperty() != null ) { else if ( rootClass.getIdentifierProperty() != null ) {
// we have the "@EmbeddedId" / <composite-id name="idName"/> case // we have the "@EmbeddedId" / <composite-id name="idName"/> case
attributeDeclarer = resolveComponentClass(); return resolveComponentClass();
} }
else { else {
// we have the "straight up" embedded (again the Hibernate term) component identifier // we have the "straight up" embedded (again the Hibernate term)
attributeDeclarer = entityClass; // component identifier: the entity class itself is the id class
return rootClass.getMappedClass();
} }
return attributeDeclarer;
} }
private Setter injector(Property property, Class<?> attributeDeclarer) { private Setter injector(Property property, Class<?> attributeDeclarer) {

View File

@ -27,6 +27,11 @@ public interface KeyValue extends Value {
boolean isUpdateable(); boolean isUpdateable();
Generator createGenerator(Dialect dialect, RootClass rootClass); @Deprecated(since = "7.0")
default Generator createGenerator(Dialect dialect, RootClass rootClass) {
return createGenerator( dialect, rootClass, null );
}
Generator createGenerator(Dialect dialect, RootClass rootClass, Property property);
} }

View File

@ -375,10 +375,10 @@ public abstract class SimpleValue implements KeyValue {
} }
@Override @Override
public Generator createGenerator(Dialect dialect, RootClass rootClass) { public Generator createGenerator(Dialect dialect, RootClass rootClass, Property property) {
if ( generator == null ) { if ( generator == null ) {
if ( customIdGeneratorCreator != null ) { if ( customIdGeneratorCreator != null ) {
generator = customIdGeneratorCreator.createGenerator( new IdGeneratorCreationContext( rootClass ) ); generator = customIdGeneratorCreator.createGenerator( new IdGeneratorCreationContext( rootClass, property ) );
} }
} }
return generator; return generator;
@ -1011,9 +1011,11 @@ public abstract class SimpleValue implements KeyValue {
private class IdGeneratorCreationContext implements GeneratorCreationContext { private class IdGeneratorCreationContext implements GeneratorCreationContext {
private final RootClass rootClass; private final RootClass rootClass;
private final Property property;
public IdGeneratorCreationContext(RootClass rootClass) { public IdGeneratorCreationContext(RootClass rootClass, Property property) {
this.rootClass = rootClass; this.rootClass = rootClass;
this.property = property;
} }
@Override @Override
@ -1048,7 +1050,7 @@ public abstract class SimpleValue implements KeyValue {
@Override @Override
public Property getProperty() { public Property getProperty() {
return rootClass.getIdentifierProperty(); return property;
} }
@Override @Override

View File

@ -598,7 +598,7 @@ public abstract class AbstractCollectionPersister
} }
private BeforeExecutionGenerator createGenerator(RuntimeModelCreationContext context, IdentifierCollection collection) { private BeforeExecutionGenerator createGenerator(RuntimeModelCreationContext context, IdentifierCollection collection) {
final Generator generator = collection.getIdentifier().createGenerator( context.getDialect(), null ); final Generator generator = collection.getIdentifier().createGenerator( context.getDialect(), null, null );
if ( generator.generatedOnExecution() ) { if ( generator.generatedOnExecution() ) {
throw new MappingException("must be an BeforeExecutionGenerator"); //TODO fix message throw new MappingException("must be an BeforeExecutionGenerator"); //TODO fix message
} }

View File

@ -0,0 +1,26 @@
package org.hibernate.orm.test.tenantidpk;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.TenantId;
import java.util.UUID;
@Entity
public class Account {
@Id @GeneratedValue Long id;
@Id @TenantId UUID tenantId;
@ManyToOne(optional = false)
Client client;
public Account(Client client) {
this.client = client;
}
Account() {}
}

View File

@ -0,0 +1,38 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantidpk;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.TenantId;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
public class Client {
@Id
@GeneratedValue
Long id;
String name;
@Id @TenantId
UUID tenantId;
@OneToMany(mappedBy = "client")
Set<Account> accounts = new HashSet<>();
public Client(String name) {
this.name = name;
}
Client() {}
}

View File

@ -0,0 +1,115 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.tenantidpk;
import org.hibernate.PropertyValueException;
import org.hibernate.binder.internal.TenantIdBinder;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryProducer;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
@SessionFactory
@DomainModel(annotatedClasses = { Account.class, Client.class })
@ServiceRegistry(
settings = {
@Setting(name = JAKARTA_HBM2DDL_DATABASE_ACTION, value = "create-drop")
}
)
public class TenantPkTest implements SessionFactoryProducer {
private static final UUID mine = UUID.randomUUID();
private static final UUID yours = UUID.randomUUID();
UUID currentTenant;
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createQuery("delete from Account").executeUpdate();
session.createQuery("delete from Client").executeUpdate();
});
}
@Override
public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) {
final SessionFactoryBuilder sessionFactoryBuilder = model.getSessionFactoryBuilder();
sessionFactoryBuilder.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver<UUID>() {
@Override
public UUID resolveCurrentTenantIdentifier() {
return currentTenant;
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
} );
return (SessionFactoryImplementor) sessionFactoryBuilder.build();
}
@Test
public void test(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
scope.inTransaction( session -> {
assertNotNull( session.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
assertEquals(mine, acc.tenantId);
currentTenant = yours;
scope.inTransaction( session -> {
assertNull( session.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.createSelectionQuery("where id=?1", Account.class)
.setParameter(1, acc.id)
.getSingleResultOrNull() );
assertEquals( 1, session.createQuery("from Account").getResultList().size() );
} );
}
@Test
public void testErrorOnInsert(SessionFactoryScope scope) {
currentTenant = mine;
Client client = new Client("Gavin");
Account acc = new Account(client);
acc.tenantId = yours;
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
} );
assertEquals( mine, acc.tenantId );
assertEquals( mine, client.tenantId );
}
}

View File

@ -9,7 +9,6 @@ package org.hibernate.processor.util.xml;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import javax.tools.FileObject; import javax.tools.FileObject;