//(c) Copyright 2011-2013 PaperCut Software Int. Pty. Ltd. http://www.papercut.com/ // // 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 pl.matisoft.soy.data; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.inject.matcher.AbstractMatcher; import com.google.inject.matcher.Matcher; import com.google.template.soy.data.SoyMapData; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import static com.google.common.collect.Lists.newArrayList; import static java.util.Collections.unmodifiableList; /** * An implementation of ToSoyDataConverter that will recursively inspect a * passed in model and build a nested structure of SoyMapData objects, which * consist only of primitives supported by Soy and thus can be rendered. * * An implementation supports passing a model object, which is wrapped in * Callable or a Future. * * In case a model wrapped in Callable is passed, the implementation will get a * wrapped model object during invocation of this method. * * In case a model wrapped in Future is passed, the implementation will * *synchronously* get a wrapped model object during invocation of this method, * assuming a 2 minutes timeout by default. If such a behaviour should be * altered, developers are requested to provider their own implementation. */ public class DefaultToSoyDataConverter implements ToSoyDataConverter { private Matcher<PropertyDescriptor> ignorablePropertiesMatcher; public DefaultToSoyDataConverter() { setIgnorablePropertiesMatcher(new DefaultIgnorablePropertiesMatcher()); } public void setIgnorablePropertiesMatcher( Matcher<PropertyDescriptor> ignorablePropertiesMatcher) { this.ignorablePropertiesMatcher = ignorablePropertiesMatcher; } @Override public Optional<SoyMapData> toSoyMap(final Object model) throws Exception { if (model == null) { return Optional.absent(); } return Optional.fromNullable(objectToSoyDataMap(model)); } @SuppressWarnings("unchecked") protected Map<String, ?> toSoyCompatibleMap(final Object obj) throws Exception { Object ret = toSoyCompatibleObjects(obj); if (!(ret instanceof Map)) { throw new IllegalArgumentException("Input should be a Map or POJO."); } return (Map<String, ?>) ret; } protected Object toSoyCompatibleObjects(Object obj) throws Exception { if (obj == null) { return obj; } if (Primitives.isWrapperType(obj.getClass()) || obj instanceof String) { return obj; } if(obj.getClass().isEnum()) { return ((Enum)obj).name(); } if (obj instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>) obj; Map<String, Object> newMap = new HashMap<String, Object>(map.size()); for (String key : map.keySet()) { newMap.put(key, toSoyCompatibleObjects(map.get(key))); } return newMap; } if (obj instanceof Iterable<?>) { List<Object> list = Lists.newArrayList(); for (Object subValue : ((Iterable<?>) obj)) { list.add(toSoyCompatibleObjects(subValue)); } return list; } if (obj instanceof Callable) { final Callable<?> callable = (Callable<?>) obj; return toSoyCompatibleObjects(callable.call()); } if (obj.getClass().isArray()) { return obj; } @SuppressWarnings("unchecked") Map<String, Object> pojoMap = (Map<String, Object>) pojoToMap(obj); Map<String, Object> newMap = new HashMap<String, Object>(pojoMap.size()); for (String key : pojoMap.keySet()) { newMap.put(key, toSoyCompatibleObjects(pojoMap.get(key))); } return newMap; } protected Map<String, ?> pojoToMap(final Object pojo) { Map<String, Object> map = new HashMap<String, Object>(); try { final BeanInfo beanInfo = Introspector.getBeanInfo(pojo.getClass()); PropertyDescriptor[] propertyDescriptors = beanInfo .getPropertyDescriptors(); for (PropertyDescriptor pd : propertyDescriptors) { if (!isIgnorable(pd)) { map.put(pd.getName(), pd.getReadMethod().invoke(pojo)); } } } catch (Exception e) { throw new RuntimeException(e); } return map; } protected SoyMapData objectToSoyDataMap(Object obj) throws Exception { if (obj == null) { return new SoyMapData(); } if (obj instanceof SoyMapData) { return (SoyMapData) obj; } return new SoyMapData(toSoyCompatibleMap(obj)); } protected boolean isIgnorable(PropertyDescriptor pd) { return ignorablePropertiesMatcher.matches(pd); } private static class DefaultIgnorablePropertiesMatcher extends AbstractMatcher<PropertyDescriptor> implements Serializable { private static final long serialVersionUID = 0; private static final List<String> ignorableProperties = unmodifiableList(newArrayList( "class", "metaClass")); @Override public boolean matches(PropertyDescriptor pd) { return pd.getReadMethod() == null || ignorableProperties.contains(pd.getName()); } } }