/**
* 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.jooby.internal.parser.bean;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import org.jooby.Request;
import org.jooby.internal.ParameterNameProvider;
import org.jooby.internal.mvc.RequestParam;
import org.jooby.internal.mvc.RequestParamNameProviderImpl;
import org.jooby.internal.mvc.RequestParamProviderImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.inject.TypeLiteral;
import javaslang.CheckedFunction1;
import javaslang.Tuple;
import javaslang.Tuple2;
@SuppressWarnings("rawtypes")
public class BeanPlan {
/** The logging system. */
private final Logger log = LoggerFactory.getLogger(Request.class);
private Constructor<?> constructor;
private List<RequestParam> parameters;
private TypeLiteral beanType;
private Map<Object, BeanPath> cache = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public BeanPlan(final ParameterNameProvider classInfo, final Class beanType) {
this(classInfo, TypeLiteral.get(beanType));
}
public BeanPlan(final ParameterNameProvider classInfo, final TypeLiteral beanType) {
Constructor<?> inject = null, def = null;
Class rawType = beanType.getRawType();
if (rawType == List.class) {
rawType = ArrayList.class;
}
Constructor<?>[] cons = rawType.getDeclaredConstructors();
if (cons.length == 1) {
def = cons[0];
} else {
for (Constructor<?> c : cons) {
if (c.isAnnotationPresent(Inject.class)) {
if (inject != null) {
throw new IllegalStateException(
"Ambigous constructor found: " + rawType.getName()
+ ". Only one @" + Inject.class.getName() + " allowed");
}
inject = c;
} else if (c.getParameterCount() == 0) {
def = c;
}
}
}
Constructor<?> constructor = inject == null ? def : inject;
if (constructor == null) {
throw new IllegalStateException("Ambigous constructor found: " + rawType.getName()
+ ". Bean/Form type must have a no-args constructor or must be annotated with @"
+ Inject.class.getName());
}
this.beanType = beanType;
this.constructor = constructor;
this.parameters = new RequestParamProviderImpl(new RequestParamNameProviderImpl(classInfo))
.parameters(constructor);
}
public Object newBean(final CheckedFunction1<RequestParam, Object> lookup,
final Set<String> params) throws Throwable {
log.debug("instantiating object {}", constructor);
Object[] args = new Object[parameters.size()];
List<String> names = new ArrayList<>(params);
// remove constructor injected params
for (int i = 0; i < args.length; i++) {
RequestParam param = parameters.get(i);
args[i] = lookup.apply(param);
// skip constructor injected param (don't override)
names.remove(param.name);
}
Object bean = constructor.newInstance(args);
List<BeanPath> paths = compile(names.stream().sorted().iterator(), beanType);
for (BeanPath path : paths) {
String rawpath = path.toString();
log.debug(" setting {}", rawpath);
path.set(bean, lookup.apply(new RequestParam(path.setelem(), rawpath, path.settype())));
}
return bean;
}
private List<BeanPath> compile(final Iterator<String> it, final TypeLiteral beanType) {
List<BeanPath> result = new ArrayList<>();
while (it.hasNext()) {
String path = it.next();
Tuple2<TypeLiteral, String> ckey = Tuple.of(beanType, path);
BeanPath cached = cache.get(ckey);
if (cached == null) {
List<Tuple2<String, Integer>> segments = segments(path);
List<BeanPath> chain = new ArrayList<>();
// traverse path
TypeLiteral ittype = beanType;
for (int i = 0; i < segments.size() - 1; i++) {
Tuple2<String, Integer> segment = segments.get(i);
final BeanPath cpath;
if (segment._2 != null) {
if (segment._1 == null) {
cpath = new BeanIndexedPath(null, segment._2, ittype);
} else {
BeanPath getter = member("get", segment._1, ittype, 0);
if (getter instanceof BeanMethodPath) {
((BeanMethodPath) getter).setter = member("set", segment._1, ittype, 1);
}
cpath = new BeanIndexedPath(getter, segment._2, ittype);
}
} else {
BeanPath getter = member("get", segment._1, ittype, 0);
if (getter instanceof BeanMethodPath) {
((BeanMethodPath) getter).setter = member("set", segment._1, ittype, 1);
}
cpath = getter;
}
if (cpath != null) {
chain.add(cpath);
ittype = TypeLiteral.get(cpath.type());
}
}
// set path
Tuple2<String, Integer> last = segments.get(segments.size() - 1);
BeanPath cpath = member("set", last._1, ittype, 1);
if (cpath != null) {
if (last._2 != null) {
BeanPath getter = member("get", last._1, ittype, 0);
if (getter instanceof BeanMethodPath) {
((BeanMethodPath) getter).setter = cpath;
}
cpath = new BeanIndexedPath(getter, last._2, ittype);
}
if (chain.size() == 0) {
cached = cpath;
} else {
cached = new BeanComplexPath(chain, cpath, path);
}
cache.put(ckey, cached);
}
}
if (cached != null) {
result.add(cached);
}
}
return result;
}
private List<Tuple2<String, Integer>> segments(final String path) {
List<String> segments = Splitter.on(CharMatcher.anyOf("[].")).trimResults()
.omitEmptyStrings()
.splitToList(path);
List<Tuple2<String, Integer>> result = new ArrayList<>(segments.size());
for (int i = 0; i < segments.size(); i++) {
String segment = segments.get(i);
try {
int idx = Integer.parseInt(segment);
if (result.size() > 0) {
result.set(result.size() - 1, Tuple.of(result.get(result.size() - 1)._1, idx));
} else {
result.add(Tuple.of(null, idx));
}
} catch (NumberFormatException x) {
result.add(Tuple.of(segment, null));
}
}
return result;
}
private BeanPath member(final String prefix, final String name, final TypeLiteral root,
final int pcount) {
Class rawType = root.getRawType();
BeanPath fn = method(prefix, name, rawType.getDeclaredMethods(), pcount);
if (fn == null) {
fn = field(name, rawType.getDeclaredFields());
// superclass lookup?
if (fn == null) {
Class<?> superclass = rawType.getSuperclass();
if (superclass != Object.class) {
return member(prefix, name, TypeLiteral.get(rawType.getGenericSuperclass()), pcount);
}
}
}
return fn;
}
private BeanFieldPath field(final String name, final Field[] fields) {
for (Field f : fields) {
if (f.getName().equals(name)) {
return new BeanFieldPath(name, f);
}
}
return null;
}
private BeanMethodPath method(final String prefix, final String name, final Method[] methods,
final int pcount) {
String bname = javaBeanMethod(new StringBuilder(prefix), name);
for (Method m : methods) {
String mname = m.getName();
if ((bname.equals(mname) || name.equals(mname)) && m.getParameterCount() == pcount) {
return new BeanMethodPath(name, m);
}
}
return null;
}
private String javaBeanMethod(final StringBuilder prefix, final String name) {
return prefix.append(Character.toUpperCase(name.charAt(0))).append(name, 1, name.length())
.toString();
}
}