/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.access.types;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.cayenne.util.Util;
/**
* Stores ExtendedTypes, implementing an algorithm to determine the right type
* for a given Java class. See {@link #getRegisteredType(String)} documentation
* for lookup algorithm details.
*/
public class ExtendedTypeMap {
static final Map<String, String> classesForPrimitives;
static {
classesForPrimitives = new HashMap<>();
classesForPrimitives.put("long", Long.class.getName());
classesForPrimitives.put("double", Double.class.getName());
classesForPrimitives.put("byte", Byte.class.getName());
classesForPrimitives.put("boolean", Boolean.class.getName());
classesForPrimitives.put("float", Float.class.getName());
classesForPrimitives.put("short", Short.class.getName());
classesForPrimitives.put("int", Integer.class.getName());
}
protected Map<String, String> typeAliases;
protected final Map<String, ExtendedType> typeMap;
protected ExtendedType defaultType;
Collection<ExtendedTypeFactory> extendedTypeFactories;
// standard type factories registered by Cayenne that are consulted after the user factories.
Collection<ExtendedTypeFactory> internalTypeFactories;
/**
* Creates new ExtendedTypeMap, populating it with default JDBC-compatible
* types. If JDK version is at least 1.5, also loads support for enumerated
* types.
*/
public ExtendedTypeMap() {
this.defaultType = new ObjectType();
this.typeMap = new ConcurrentHashMap<>();
this.typeAliases = new ConcurrentHashMap<>(classesForPrimitives);
this.extendedTypeFactories = new CopyOnWriteArrayList<>();
this.internalTypeFactories = new CopyOnWriteArrayList<>();
initDefaultFactories();
}
/**
* Registers default factories for creating enum types and serializable
* types. Note that user-defined factories are consulted before any default
* factory.
*
* @since 3.0
*/
protected void initDefaultFactories() {
internalTypeFactories.add(new EnumTypeFactory());
internalTypeFactories.add(new ByteOrCharArrayFactory(this));
// note that Serializable type should be used as a last resort after all
// other alternatives are exhausted.
internalTypeFactories.add(new SerializableTypeFactory(this));
}
/**
* Adds an ExtendedTypeFactory that will be consulted if no direct mapping
* for a given class exists. This feature can be used to map interfaces.
* <p>
* <i>Note that the order in which factories are added is important, as
* factories are consulted in turn when an ExtendedType is looked up, and
* lookup is stopped when any factory provides a non-null type.</i>
* </p>
*
* @since 1.2
*/
public void addFactory(ExtendedTypeFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("Attempt to add null factory");
}
extendedTypeFactories.add(factory);
}
/**
* Removes a factory from the registered factories if it was previously
* added.
*
* @since 1.2
*/
public void removeFactory(ExtendedTypeFactory factory) {
if (factory != null) {
extendedTypeFactories.remove(factory);
}
}
/**
* Adds a new type to the list of registered types. If there is another type
* registered for a class described by the <code>type</code> argument, the
* old handler is overridden by the new one.
*/
public void registerType(ExtendedType type) {
typeMap.put(type.getClassName(), type);
// factory to handle subclasses of type.className
addFactory(new SubclassTypeFactory(type));
}
/**
* Returns a default ExtendedType that is used to handle unmapped types.
*/
public ExtendedType getDefaultType() {
return defaultType;
}
/**
* Returns a guaranteed non-null ExtendedType instance for a given Java
* class name. Primitive class names are internally replaced by the
* non-primitive counterparts. The following lookup sequence is used to
* determine the type:
* <ul>
* <li>First the methods checks for an ExtendedType explicitly registered
* with the map for a given class name (most common types are registered by
* Cayenne internally; users can register their own).</li>
* <li>Second, the method tries to obtain a type by iterating through
* {@link ExtendedTypeFactory} instances registered by users. If a factory
* returns a non-null type, it is returned to the user and the rest of the
* factories are ignored.</li>
* <li>Third, the method iterates through standard
* {@link ExtendedTypeFactory} instances that can dynamically construct
* extended types for serializable objects and JDK 1.5 enums.</li>
* <li>If all the methods above failed, the default type is returned that
* relies on default JDBC driver mapping to set and get objects.</li>
* </ul>
* <i>Note that for array types class name must be in the form
* 'MyClass[]'</i>.
*/
public ExtendedType getRegisteredType(String javaClassName) {
if (javaClassName == null) {
return getDefaultType();
}
javaClassName = canonicalizedTypeName(javaClassName);
ExtendedType type = getExplictlyRegisteredType(javaClassName);
if (type != null) {
return type;
}
type = createType(javaClassName);
if (type != null) {
// register to speed up future access
registerType(type);
return type;
}
return getDefaultType();
}
ExtendedType getExplictlyRegisteredType(String className) {
if (className == null) {
throw new NullPointerException("Null className");
}
return typeMap.get(className);
}
/**
* Returns a type registered for the class name. If no such type exists,
* returns the default type. It is guaranteed that this method returns a
* non-null ExtendedType instance.
*/
public ExtendedType getRegisteredType(Class<?> javaClass) {
return getRegisteredType(javaClass.getCanonicalName());
}
/**
* Removes registered ExtendedType object corresponding to
* <code>javaClassName</code> parameter.
*/
public void unregisterType(String javaClassName) {
typeMap.remove(javaClassName);
}
/**
* Returns array of Java class names supported by Cayenne for JDBC mapping.
*/
public String[] getRegisteredTypeNames() {
Set<String> keys = typeMap.keySet();
int len = keys.size();
String[] types = new String[len];
Iterator<String> it = keys.iterator();
for (int i = 0; i < len; i++) {
types[i] = it.next();
}
return types;
}
/**
* Returns an ExtendedType for specific Java classes. Uses user-provided and
* Cayenne-provided {@link ExtendedTypeFactory} factories to instantiate the
* ExtendedType. All primitive classes must be converted to the
* corresponding Java classes by the callers.
*
* @return a default type for a given class or null if a class has no
* default type mapping.
* @since 1.2
*/
protected ExtendedType createType(String className) {
if (className == null) {
return null;
}
Class<?> typeClass;
try {
typeClass = Util.getJavaClass(className);
} catch (Throwable th) {
// ignore exceptions...
return null;
}
// lookup in user factories first
for (ExtendedTypeFactory factory : extendedTypeFactories) {
ExtendedType type = factory.getType(typeClass);
if (type != null) {
return type;
}
}
// lookup in internal factories
for (ExtendedTypeFactory factory : internalTypeFactories) {
ExtendedType type = factory.getType(typeClass);
if (type != null) {
return type;
}
}
return null;
}
/**
* For the class name returns a name "canonicalized" for the purpose of
* ExtendedType lookup.
*
* @since 4.0
*/
protected String canonicalizedTypeName(String className) {
String canonicalized = typeAliases.get(className);
if (canonicalized == null) {
int index = className.indexOf('$');
if (index >= 0) {
canonicalized = className.replace('$', '.');
} else {
canonicalized = className;
}
typeAliases.put(className, canonicalized);
}
return canonicalized;
}
}