/*
* Copyright 2017 MongoDB, 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 org.bson.codecs.pojo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.lang.String.format;
import static org.bson.assertions.Assertions.notNull;
final class TypeData<T> {
private final Class<T> type;
private final List<TypeData<?>> typeParameters;
/**
* Creates a new builder for ClassTypeData
*
* @param type the class for the type
* @param <T> the type
* @return the builder
*/
public static <T> Builder<T> builder(final Class<T> type) {
return new Builder<T>(notNull("type", type));
}
/**
* @return the class this {@code ClassTypeData} represents
*/
public Class<T> getType() {
return type;
}
/**
* @return the type parameters for the class
*/
public List<TypeData<?>> getTypeParameters() {
return typeParameters;
}
/**
* A builder for TypeData
*
* @param <T> the main type
*/
public static final class Builder<T> {
private final Class<T> type;
private final List<TypeData<?>> typeParameters = new ArrayList<TypeData<?>>();
private Builder(final Class<T> type) {
this.type = type;
}
/**
* Adds a type parameter
*
* @param typeParameter the type parameter
* @param <S> the type of the type parameter
* @return this
*/
public <S> Builder<T> addTypeParameter(final TypeData<S> typeParameter) {
typeParameters.add(notNull("typeParameter", typeParameter));
return this;
}
/**
* Adds multiple type parameters
*
* @param typeParameters the type parameters
* @return this
*/
public Builder<T> addTypeParameters(final List<TypeData<?>> typeParameters) {
notNull("typeParameters", typeParameters);
for (TypeData<?> typeParameter : typeParameters) {
addTypeParameter(typeParameter);
}
return this;
}
/**
* @return the class type data
*/
public TypeData<T> build() {
validate();
return new TypeData<T>(type, Collections.unmodifiableList(typeParameters));
}
private void validate() {
if (Collection.class.isAssignableFrom(type)) {
if (typeParameters.size() != 1) {
throw new IllegalStateException("Invalid Collection type. Collections must have a single type parameter defined.");
}
} else if (Map.class.isAssignableFrom(type)) {
if (typeParameters.size() != 2) {
throw new IllegalStateException("Invalid Map type. Map must have two type parameters defined.");
} else if (typeParameters.get(0).getType() != String.class) {
throw new IllegalStateException(format("Invalid Map type. Maps MUST have string keys, found %s instead.",
typeParameters.get(0).getType()));
}
}
}
}
@Override
public String toString() {
String typeParams = typeParameters.isEmpty() ? ""
: ", typeParameters=[" + nestedTypeParameters(typeParameters) + "]";
return "TypeData{"
+ "type=" + type.getSimpleName()
+ typeParams
+ "}";
}
private static String nestedTypeParameters(final List<TypeData<?>> typeParameters) {
StringBuilder builder = new StringBuilder();
int count = 0;
int last = typeParameters.size();
for (TypeData<?> typeParameter : typeParameters) {
count++;
builder.append(typeParameter.getType().getSimpleName());
if (!typeParameter.getTypeParameters().isEmpty()) {
builder.append(format("<%s>", nestedTypeParameters(typeParameter.getTypeParameters())));
}
if (count < last) {
builder.append(", ");
}
}
return builder.toString();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TypeData)) {
return false;
}
TypeData<?> that = (TypeData<?>) o;
if (!getType().equals(that.getType())) {
return false;
}
if (!getTypeParameters().equals(that.getTypeParameters())) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = getType().hashCode();
result = 31 * result + getTypeParameters().hashCode();
return result;
}
private TypeData(final Class<T> type, final List<TypeData<?>> typeParameters) {
this.type = getClass(type);
this.typeParameters = typeParameters;
}
@SuppressWarnings("unchecked")
private Class<T> getClass(final Class<T> type) {
Class<T> instanceType = boxType(type);
if (type.equals(Map.class)) {
instanceType = (Class<T>) HashMap.class;
} else if (type.equals(List.class) || type.equals(Collection.class)) {
instanceType = (Class<T>) ArrayList.class;
} else if (type.equals(Set.class)) {
instanceType = (Class<T>) HashSet.class;
}
return instanceType;
}
@SuppressWarnings("unchecked")
private Class<T> boxType(final Class<T> clazz) {
if (clazz.isPrimitive()) {
return (Class<T>) PRIMITIVE_CLASS_MAP.get(clazz);
} else {
return clazz;
}
}
private static final Map<Class<?>, Class<?>> PRIMITIVE_CLASS_MAP;
static {
Map<Class<?>, Class<?>> map = new HashMap<Class<?>, Class<?>>();
map.put(boolean.class, Boolean.class);
map.put(byte.class, Byte.class);
map.put(char.class, Character.class);
map.put(double.class, Double.class);
map.put(float.class, Float.class);
map.put(int.class, Integer.class);
map.put(long.class, Long.class);
map.put(short.class, Short.class);
PRIMITIVE_CLASS_MAP = map;
}
}