com.datastax.driver.mapping.MappingManager Maven / Gradle / Ivy
/*
* Copyright DataStax, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datastax.driver.mapping;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.SchemaChangeListenerBase;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.TableMetadata;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.UserType;
import com.datastax.driver.core.utils.MoreObjects;
import com.datastax.driver.mapping.annotations.Accessor;
import com.datastax.driver.mapping.annotations.Table;
import com.datastax.driver.mapping.annotations.UDT;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Mapping manager from which to obtain entity mappers. */
public class MappingManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MappingManager.class);
private final Session session;
private final MappingConfiguration configuration;
final int protocolVersionAsInt;
private final ConcurrentHashMap> mappers =
new ConcurrentHashMap>();
private final ConcurrentHashMap> udtCodecs =
new ConcurrentHashMap>();
private final ConcurrentHashMap, Object> accessors =
new ConcurrentHashMap, Object>();
/**
* Creates a new {@code MappingManager} using the provided {@code Session} with default {@code
* MapperConfiguration}.
*
* Note that this constructor forces the initialization of the session (see {@link
* #MappingManager(Session, ProtocolVersion)} if that is a problem for you).
*
* @param session the {@code Session} to use.
*/
public MappingManager(Session session) {
this(session, getProtocolVersion(session));
}
private static ProtocolVersion getProtocolVersion(Session session) {
session.init();
return session.getCluster().getConfiguration().getProtocolOptions().getProtocolVersion();
}
/**
* Creates a new {@code MappingManager} using the provided {@code Session} with default {@code
* MapperConfiguration}.
*
*
This constructor is only provided for backward compatibility: before 2.1.7, {@code
* MappingManager} could be built from an uninitialized session; since 2.1.7, the mapper needs to
* know the active protocol version to adapt its internal requests, so {@link
* #MappingManager(Session)} will now initialize the session if needed. If you rely on the session
* not being initialized, use this constructor and provide the version manually.
*
* @param session the {@code Session} to use.
* @param protocolVersion the protocol version that will be used with this session.
* @since 2.1.7
*/
public MappingManager(Session session, ProtocolVersion protocolVersion) {
this(session, MappingConfiguration.builder().build(), protocolVersion);
}
/**
* Creates a new {@code MappingManager} using the provided {@code Session} with custom
* configuration to be inherited to each instantiated mapper.
*
*
Note that this constructor forces the initialization of the session (see {@link
* #MappingManager(Session, ProtocolVersion)} if that is a problem for you).
*
* @param session the {@code Session} to use.
* @param configuration the {@code MapperConfiguration} to use be used as default for instantiated
* mappers.
*/
public MappingManager(Session session, MappingConfiguration configuration) {
this(session, configuration, getProtocolVersion(session));
}
/**
* Creates a new {@code MappingManager} using the provided {@code Session} with default {@code
* MapperConfiguration}.
*
*
This constructor is only provided for backward compatibility: before 2.1.7, {@code
* MappingManager} could be built from an uninitialized session; since 2.1.7, the mapper needs to
* know the active protocol version to adapt its internal requests, so {@link
* #MappingManager(Session)} will now initialize the session if needed. If you rely on the session
* not being initialized, use this constructor and provide the version manually.
*
* @param session the {@code Session} to use.
* @param configuration the {@code MapperConfiguration} to use be used as default for instantiated
* mappers.
* @param protocolVersion the protocol version that will be used with this session.
*/
public MappingManager(
Session session, MappingConfiguration configuration, ProtocolVersion protocolVersion) {
this.session = session;
this.configuration = configuration;
// This is not strictly correct in clusters with mixed C* node versions, which typically can
// occur when upgrading to
// a major version in Cassandra that has a protocol change.
// But mappers need to make a decision early so that generated queries are compatible, and we
// don't know in advance
// which nodes might join the cluster later.
// At least if protocol >=2 we know there won't be any 1.2 nodes ever.
this.protocolVersionAsInt = protocolVersion.toInt();
session
.getCluster()
.register(
new SchemaChangeListenerBase() {
@Override
public void onTableRemoved(TableMetadata table) {
synchronized (mappers) {
Iterator> it = mappers.values().iterator();
while (it.hasNext()) {
Mapper> mapper = it.next();
if (mapper.getTableMetadata().equals(table)) {
LOGGER.error(
"Table {} has been removed; existing mappers for @Entity annotated {} will not work anymore",
table.getName(),
mapper.getMappedClass());
it.remove();
}
}
}
}
@Override
public void onTableChanged(TableMetadata current, TableMetadata previous) {
synchronized (mappers) {
Iterator> it = mappers.values().iterator();
while (it.hasNext()) {
Mapper> mapper = it.next();
if (mapper.getTableMetadata().equals(previous)) {
LOGGER.warn(
"Table {} has been altered; existing mappers for @Entity annotated {} might not work properly anymore",
previous.getName(),
mapper.getMappedClass());
it.remove();
}
}
}
}
@Override
public void onUserTypeRemoved(UserType type) {
synchronized (udtCodecs) {
Iterator> it = udtCodecs.values().iterator();
while (it.hasNext()) {
MappedUDTCodec> codec = it.next();
if (type.equals(codec.getCqlType())) {
LOGGER.error(
"User type {} has been removed; existing mappers for @UDT annotated {} will not work anymore",
type,
codec.getUdtClass());
it.remove();
}
}
}
}
@Override
public void onUserTypeChanged(UserType current, UserType previous) {
synchronized (udtCodecs) {
Set deletedCodecs = new HashSet();
Iterator>> it =
udtCodecs.entrySet().iterator();
while (it.hasNext()) {
Map.Entry> entry = it.next();
MappedUDTCodec> codec = entry.getValue();
if (previous.equals(codec.getCqlType())) {
LOGGER.warn(
"User type {} has been altered; existing mappers for @UDT annotated {} might not work properly anymore",
previous,
codec.getUdtClass());
deletedCodecs.add(entry.getKey());
it.remove();
}
}
for (CacheKey key : deletedCodecs) {
// try to register an updated version of the previous codec
try {
getUDTCodec(key.klass, key.keyspace);
} catch (Exception e) {
LOGGER.error("Could not update mapping for @UDT annotated " + key.klass, e);
}
}
}
}
});
}
/**
* The underlying {@link Session} used by this manager.
*
* Note that you can get obtain the {@link Cluster} object corresponding to that session using
* {@code getSession().getCluster()}.
*
*
It is inadvisable to close the returned Session while this manager and its mappers are in
* use.
*
* @return the underlying session used by this manager.
*/
public Session getSession() {
return session;
}
/**
* Returns the {@link MappingConfiguration configuration} used by this manager.
*
* @return the {@link MappingConfiguration configuration} used by this manager.
*/
public MappingConfiguration getConfiguration() {
return configuration;
}
/**
* Creates a {@code Mapper} for the provided class (that must be annotated by a {@link Table}
* annotation).
*
*
The {@code MappingManager} only ever keeps one Mapper for each class and keyspace, and so
* calling this method multiple times on the same class will always return the same object.
*
*
If the type of any field in the class is an {@link UDT}-annotated classes, a codec for that
* class will automatically be created and registered with the underlying {@code Cluster}. This
* works recursively with UDTs nested in other UDTs or in collections.
*
* @param the type of the class to map.
* @param klass the (annotated) class for which to return the mapper.
* @param keyspace the target keyspace for the mapping (must be quoted if case-sensitive). If this
* is {@code null}, the mapper will try to use the keyspace defined by the annotation, or the
* default keyspace on the session, or fail if neither is declared.
*/
public Mapper mapper(Class klass, String keyspace) {
return getMapper(klass, keyspace);
}
/**
* Creates a {@code Mapper} for the provided class, using the default keyspace (either the one
* declared in the {@link Table} annotation, or the logged keyspace on the session).
*
* This is equivalent to {@code this.mapper(klass, null)}.
*
* @see #mapper(Class, String)
*/
public Mapper mapper(Class klass) {
return mapper(klass, null);
}
/**
* Creates a {@code TypeCodec} for the provided class (that must be annotated by a {@link UDT}
* annotation).
*
* This method also registers the codec against the underlying {@code Cluster}. In addition,
* the codecs for any nested UDTs will also be created and registered.
*
*
You don't need to call this method explicitly if you already call {@link #mapper(Class)} for
* a class that references this UDT class (creating a mapper will automatically process all UDTs
* that it uses).
*
* @param the type of the class to map.
* @param klass the (annotated) class for which to return the codec.
* @param keyspace the target keyspace for the mapping (must be quoted if case-sensitive). If this
* is {@code null}, the mapper will try to use the keyspace defined by the annotation, or the
* default keyspace on the session, or fail if neither is declared.
*/
public TypeCodec udtCodec(Class klass, String keyspace) {
return getUDTCodec(klass, keyspace);
}
/**
* Creates a {@code TypeCodec} for the provided class, using the default keyspace (either the one
* declared in the {@link Table} annotation, or the logged keyspace on the session).
*
* This is equivalent to {@code this.udtCodec(klass, null)}.
*
* @see #udtCodec(Class, String)
*/
public TypeCodec udtCodec(Class klass) {
return udtCodec(klass, null);
}
/**
* Creates an accessor object based on the provided interface (that must be annotated by a {@link
* Accessor} annotation).
*
* The {@code MappingManager} only ever keep one Accessor for each class, and so calling this
* method multiple time on the same class will always return the same object.
*
* @param the type of the accessor class.
* @param klass the (annotated) class for which to create an accessor object.
* @return the accessor object for class {@code klass}.
*/
public T createAccessor(Class klass) {
return getAccessor(klass);
}
@SuppressWarnings("unchecked")
private Mapper getMapper(Class klass, String keyspace) {
CacheKey cacheKey = new CacheKey(klass, keyspace);
Mapper mapper = (Mapper) mappers.get(cacheKey);
if (mapper == null) {
EntityMapper entityMapper = AnnotationParser.parseEntity(klass, keyspace, this);
mapper = new Mapper(this, klass, entityMapper);
Mapper old = (Mapper) mappers.putIfAbsent(cacheKey, mapper);
if (old != null) {
mapper = old;
}
}
return mapper;
}
@SuppressWarnings("unchecked")
TypeCodec getUDTCodec(Class mappedClass, String keyspace) {
CacheKey cacheKey = new CacheKey(mappedClass, keyspace);
MappedUDTCodec codec = (MappedUDTCodec) udtCodecs.get(cacheKey);
if (codec == null) {
codec = AnnotationParser.parseUDT(mappedClass, keyspace, this);
session.getCluster().getConfiguration().getCodecRegistry().register(codec);
MappedUDTCodec old = (MappedUDTCodec) udtCodecs.putIfAbsent(cacheKey, codec);
if (old != null) {
codec = old;
}
}
return codec;
}
@SuppressWarnings("unchecked")
private T getAccessor(Class klass) {
T accessor = (T) accessors.get(klass);
if (accessor == null) {
AccessorMapper mapper = AnnotationParser.parseAccessor(klass, this);
mapper.prepare(this);
accessor = mapper.createProxy();
T old = (T) accessors.putIfAbsent(klass, accessor);
if (old != null) {
accessor = old;
}
}
return accessor;
}
private static class CacheKey {
final Class> klass;
final String keyspace;
CacheKey(Class> klass, String keyspace) {
this.klass = klass;
this.keyspace = keyspace;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
} else if (other instanceof CacheKey) {
CacheKey that = (CacheKey) other;
return this.klass.equals(that.klass) && MoreObjects.equal(this.keyspace, that.keyspace);
} else {
return false;
}
}
@Override
public int hashCode() {
return MoreObjects.hashCode(klass, keyspace);
}
}
}