/*
* Copyright (c) 2010 Google 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.google.api.client.util;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.WeakHashMap;
/**
* Computes class information to determine data key name/value pairs associated with the class.
*
* <p>
* Implementation is thread-safe.
* </p>
*
* @since 1.0
* @author Yaniv Inbar
*/
public final class ClassInfo {
/** Class information cache, with case-sensitive field names. */
private static final Map<Class<?>, ClassInfo> CACHE = new WeakHashMap<Class<?>, ClassInfo>();
/** Class information cache, with case-insensitive fields names. */
private static final Map<Class<?>, ClassInfo> CACHE_IGNORE_CASE =
new WeakHashMap<Class<?>, ClassInfo>();
/** Class. */
private final Class<?> clazz;
/** Whether field names are case sensitive. */
private final boolean ignoreCase;
/** Map from {@link FieldInfo#getName()} to the field information. */
private final IdentityHashMap<String, FieldInfo> nameToFieldInfoMap =
new IdentityHashMap<String, FieldInfo>();
/**
* Unmodifiable sorted (with any possible {@code null} member first) list (without duplicates) of
* {@link FieldInfo#getName()}.
*/
final List<String> names;
/**
* Returns the class information for the given underlying class.
*
* @param underlyingClass underlying class or {@code null} for {@code null} result
* @return class information or {@code null} for {@code null} input
*/
public static ClassInfo of(Class<?> underlyingClass) {
return of(underlyingClass, false);
}
/**
* Returns the class information for the given underlying class.
*
* @param underlyingClass underlying class or {@code null} for {@code null} result
* @param ignoreCase whether field names are case sensitive
* @return class information or {@code null} for {@code null} input
* @since 1.10
*/
public static ClassInfo of(Class<?> underlyingClass, boolean ignoreCase) {
if (underlyingClass == null) {
return null;
}
final Map<Class<?>, ClassInfo> cache = ignoreCase ? CACHE_IGNORE_CASE : CACHE;
ClassInfo classInfo;
synchronized (cache) {
classInfo = cache.get(underlyingClass);
if (classInfo == null) {
classInfo = new ClassInfo(underlyingClass, ignoreCase);
cache.put(underlyingClass, classInfo);
}
}
return classInfo;
}
/**
* Returns the underlying class.
*
* @since 1.4
*/
public Class<?> getUnderlyingClass() {
return clazz;
}
/**
* Returns whether field names are case sensitive.
*
* @since 1.10
*/
public final boolean getIgnoreCase() {
return ignoreCase;
}
/**
* Returns the information for the given {@link FieldInfo#getName()}.
*
* @param name {@link FieldInfo#getName()} or {@code null}
* @return field information or {@code null} for none
*/
public FieldInfo getFieldInfo(String name) {
if (name != null) {
if (ignoreCase) {
name = name.toLowerCase();
}
name = name.intern();
}
return nameToFieldInfoMap.get(name);
}
/**
* Returns the field for the given {@link FieldInfo#getName()}.
*
* @param name {@link FieldInfo#getName()} or {@code null}
* @return field or {@code null} for none
*/
public Field getField(String name) {
FieldInfo fieldInfo = getFieldInfo(name);
return fieldInfo == null ? null : fieldInfo.getField();
}
/**
* Returns the underlying class is an enum.
*
* @since 1.4
*/
public boolean isEnum() {
return clazz.isEnum();
}
/**
* Returns an unmodifiable sorted set (with any possible {@code null} member first) of
* {@link FieldInfo#getName() names}.
*/
public Collection<String> getNames() {
return names;
}
private ClassInfo(Class<?> srcClass, boolean ignoreCase) {
clazz = srcClass;
this.ignoreCase = ignoreCase;
Preconditions.checkArgument(
!ignoreCase || !srcClass.isEnum(), "cannot ignore case on an enum: " + srcClass);
// name set has a special comparator to keep null first
TreeSet<String> nameSet = new TreeSet<String>(new Comparator<String>() {
public int compare(String s0, String s1) {
return s0 == s1 ? 0 : s0 == null ? -1 : s1 == null ? 1 : s0.compareTo(s1);
}
});
// iterate over declared fields
for (Field field : srcClass.getDeclaredFields()) {
FieldInfo fieldInfo = FieldInfo.of(field);
if (fieldInfo == null) {
continue;
}
String fieldName = fieldInfo.getName();
if (ignoreCase) {
fieldName = fieldName.toLowerCase().intern();
}
FieldInfo conflictingFieldInfo = nameToFieldInfoMap.get(fieldName);
Preconditions.checkArgument(conflictingFieldInfo == null,
"two fields have the same %sname <%s>: %s and %s",
ignoreCase ? "case-insensitive " : "",
fieldName,
field,
conflictingFieldInfo == null ? null : conflictingFieldInfo.getField());
nameToFieldInfoMap.put(fieldName, fieldInfo);
nameSet.add(fieldName);
}
// inherit from super class and add only fields not already in nameToFieldInfoMap
Class<?> superClass = srcClass.getSuperclass();
if (superClass != null) {
ClassInfo superClassInfo = ClassInfo.of(superClass, ignoreCase);
nameSet.addAll(superClassInfo.names);
for (Map.Entry<String, FieldInfo> e : superClassInfo.nameToFieldInfoMap.entrySet()) {
String name = e.getKey();
if (!nameToFieldInfoMap.containsKey(name)) {
nameToFieldInfoMap.put(name, e.getValue());
}
}
}
names = nameSet.isEmpty() ? Collections.<String>emptyList() : Collections.unmodifiableList(
new ArrayList<String>(nameSet));
}
/**
* Returns an unmodifiable collection of the {@code FieldInfo}s for this class, without any
* guarantee of order.
*
* <p>
* If you need sorted order, instead use {@link #getNames()} with {@link #getFieldInfo(String)}.
* </p>
*
* @since 1.16
*/
public Collection<FieldInfo> getFieldInfos() {
return Collections.unmodifiableCollection(nameToFieldInfoMap.values());
}
}