/**
* Copyright (C) 2014 CUSTIS (http://www.custis.ru/)
*
* 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 ru.custis.beanpath;
import com.google.common.reflect.TypeToken;
import ru.custis.beanpath.MockMaker.InvocationCallback;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Character.isUpperCase;
/**
* This is where all the magic resides :-)
*/
public final class BeanPathMagic {
private BeanPathMagic() {}
@SuppressWarnings("unchecked")
public static @Nonnull <T> T root(@Nonnull Class<T> clazz) {
checkNotNull(clazz, "Argument 'clazz' must not be null");
return (T) Mocker.mock(TypeToken.of(clazz));
}
@SuppressWarnings("unchecked")
public static @Nonnull <T> T root(@Nonnull TypeLiteral<T> type) {
checkNotNull(type, "Argument 'type' must not be null");
return (T) Mocker.mock(type.toTypeToken());
}
@SuppressWarnings({"unchecked", "UnusedParameters"})
public static @Nonnull <T> BeanPath<T> $(T callChain) {
final BeanPath<?> path = CurrentPath.evict();
if (path == null) {
throw new BeanPathMagicException("No current path. Probably your call chain contains a final method.");
}
return (BeanPath<T>) path;
}
public static @Nonnull String $$(Object callChain) {
return $(callChain).toDotDelimitedString();
}
private static final class Mocker {
private static final Map<TypeToken, Object> cache = new ConcurrentHashMap<TypeToken, Object>();
private static final Object mockCreationGuard = new Object();
@SuppressWarnings("unchecked")
public static <T> T mock(TypeToken type) {
Object mock = cache.get(type);
if (mock == null) {
synchronized (mockCreationGuard) { // we do not want to generate a mock twice
mock = cache.get(type);
if (mock == null) {
try {
mock = MockMaker.createMock(type.getRawType(), new MockInvocationHandler(type));
} catch (Exception x) {
throw new BeanPathMagicException("Failed to mock type [%s]", type, x);
}
cache.put(type, mock);
}
}
}
return (T) mock;
}
private static class MockInvocationHandler implements InvocationCallback {
// rawMockType can be inferred from mockType,
// but TypeToken.getRawType() is relatively slow,
// so avoid it in time critical invoke()
private final TypeToken mockType;
private final Class rawMockType;
private MockInvocationHandler(TypeToken mockType) {
this.mockType = mockType;
this.rawMockType = mockType.getRawType();
}
@Override
public Object invoke(Object target, Method method, Object[] args) throws Throwable {
CurrentPath.initIfNotAlready(rawMockType);
final Type genericReturnType = method.getGenericReturnType();
Class rawReturnType = method.getReturnType();
TypeToken returnType;
// again, TypeToken.getRawType() is slow, avoid it in simple cases
if (genericReturnType == rawReturnType) {
returnType = TypeToken.of(rawReturnType);
} else {
returnType = mockType.resolveType(genericReturnType);
rawReturnType = returnType.getRawType();
}
final String name = NameUtils.stripGetIsPrefixIfAny(method.getName());
final Class type = rawReturnType.isPrimitive() ? Primitives.getWrapperClass(rawReturnType) : rawReturnType;
CurrentPath.append(name, type);
if (rawReturnType.isPrimitive()) {
// including void.class, that makes no sense,
// but anyway we can handle it
return Primitives.getDefaultValue(rawReturnType);
} else if (Modifier.isFinal(rawReturnType.getModifiers())) {
// for String, primitive wrappers, enums and arrays,
// that we can't proxy, but must handle
// when they close property chain
return null;
} else {
return mock(returnType);
}
}
}
}
private static final class CurrentPath {
private static final ThreadLocal<BeanPath<?>> currentPathTL = new ThreadLocal<BeanPath<?>>();
public static void initIfNotAlready(Class<?> clazz) {
if (currentPathTL.get() == null) {
currentPathTL.set(BeanPath.root(clazz));
}
}
public static void append(String name, Class<?> type) {
final BeanPath<?> path = currentPathTL.get();
assert (path != null);
currentPathTL.set(path.append(name, type));
}
public static BeanPath<?> evict() {
final BeanPath<?> path = currentPathTL.get();
currentPathTL.set(null);
return path;
}
}
private static final class NameUtils {
private static final String IS = "is", GET = "get";
public static String stripGetIsPrefixIfAny(final String name) {
assert (name != null);
if (name.length() > GET.length() && name.startsWith(GET) && isUpperCase(name.charAt(GET.length()))) {
return stripAndDecapitalize(name, GET);
} else if (name.length() > IS.length() && name.startsWith(IS) && isUpperCase(name.charAt(IS.length()))) {
return stripAndDecapitalize(name, IS);
}
return name;
}
private static String stripAndDecapitalize(String name, String prefix) {
final int nameLength = name.length();
final int prefixLength = prefix.length();
final int i = prefixLength + 1;
if (nameLength <= i || !isUpperCase(name.charAt(i))) {
final char chars[] = name.toCharArray();
chars[prefixLength] = Character.toLowerCase(chars[prefixLength]);
return new String(chars, prefixLength, nameLength - prefixLength);
} else {
return name.substring(prefixLength);
}
}
}
}