http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java new file mode 100644 index 0000000..9c483d6 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java @@ -0,0 +1,362 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core._DelayedFTLTypeDescription; +import org.apache.freemarker.core._DelayedJQuote; +import org.apache.freemarker.core._TemplateModelException; +import org.apache.freemarker.core.model.AdapterTemplateModel; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.TemplateModelWithAPISupport; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.WrapperTemplateModel; +import org.apache.freemarker.core.util._StringUtil; +import org.slf4j.Logger; + +/** + * A class that will wrap an arbitrary object into {@link org.apache.freemarker.core.model.TemplateHashModel} + * interface allowing calls to arbitrary property getters and invocation of + * accessible methods on the object from a template using the + * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to + * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to + * access indexed properties. It uses Beans {@link java.beans.Introspector} + * to dynamically discover the properties and methods. + */ + +public class BeanModel +implements + TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport { + + private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER; + + protected final Object object; + protected final DefaultObjectWrapper wrapper; + + // We use this to represent an unknown value as opposed to known value of null (JR) + static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN"); + + static final ModelFactory FACTORY = + new ModelFactory() + { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new BeanModel(object, (DefaultObjectWrapper) wrapper); + } + }; + + // I've tried to use a volatile ConcurrentHashMap field instead of HashMap + synchronized(this), but oddly it was + // a bit slower, at least on Java 8 u66. + private HashMap<Object, TemplateModel> memberCache; + + /** + * Creates a new model that wraps the specified object. Note that there are + * specialized subclasses of this class for wrapping arrays, collections, + * enumeration, iterators, and maps. Note also that the superclass can be + * used to wrap String objects if only scalar functionality is needed. You + * can also choose to delegate the choice over which model class is used for + * wrapping to {@link DefaultObjectWrapper#wrap(Object)}. + * @param object the object to wrap into a model. + * @param wrapper the {@link DefaultObjectWrapper} associated with this model. + * Every model has to have an associated {@link DefaultObjectWrapper} instance. The + * model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public BeanModel(Object object, DefaultObjectWrapper wrapper) { + // [2.4]: All models were introspected here, then the results was discareded, and get() will just do the + // introspection again. So is this necessary? (The inrospectNow parameter was added in 2.3.21 to allow + // lazy-introspecting DefaultObjectWrapper.trueModel|falseModel.) + this(object, wrapper, true); + } + + /** @since 2.3.21 */ + BeanModel(Object object, DefaultObjectWrapper wrapper, boolean inrospectNow) { + this.object = object; + this.wrapper = wrapper; + if (inrospectNow && object != null) { + // [2.4]: Could this be removed? + wrapper.getClassIntrospector().get(object.getClass()); + } + } + + /** + * Uses Beans introspection to locate a property or method with name + * matching the key name. If a method or property is found, it's wrapped + * into {@link org.apache.freemarker.core.model.TemplateMethodModelEx} (for a method or + * indexed property), or evaluated on-the-fly and the return value wrapped + * into appropriate model (for a simple property) Models for various + * properties and methods are cached on a per-class basis, so the costly + * introspection is performed only once per property or method of a class. + * (Side-note: this also implies that any class whose method has been called + * will be strongly referred to by the framework and will not become + * unloadable until this class has been unloaded first. Normally this is not + * an issue, but can be in a rare scenario where you create many classes on- + * the-fly. Also, as the cache grows with new classes and methods introduced + * to the framework, it may appear as if it were leaking memory. The + * framework does, however detect class reloads (if you happen to be in an + * environment that does this kind of things--servlet containers do it when + * they reload a web application) and flushes the cache. If no method or + * property matching the key is found, the framework will try to invoke + * methods with signature + * <tt>non-void-return-type get(java.lang.String)</tt>, + * then <tt>non-void-return-type get(java.lang.Object)</tt>, or + * alternatively (if the wrapped object is a resource bundle) + * <tt>Object getObject(java.lang.String)</tt>. + * @throws TemplateModelException if there was no property nor method nor + * a generic <tt>get</tt> method to invoke. + */ + @Override + public TemplateModel get(String key) + throws TemplateModelException { + Class<?> clazz = object.getClass(); + Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz); + TemplateModel retval = null; + + try { + if (wrapper.isMethodsShadowItems()) { + Object fd = classInfo.get(key); + if (fd != null) { + retval = invokeThroughDescriptor(fd, classInfo); + } else { + retval = invokeGenericGet(classInfo, clazz, key); + } + } else { + TemplateModel model = invokeGenericGet(classInfo, clazz, key); + final TemplateModel nullModel = wrapper.wrap(null); + if (model != nullModel && model != UNKNOWN) { + return model; + } + Object fd = classInfo.get(key); + if (fd != null) { + retval = invokeThroughDescriptor(fd, classInfo); + if (retval == UNKNOWN && model == nullModel) { + // This is the (somewhat subtle) case where the generic get() returns null + // and we have no bean info, so we respect the fact that + // the generic get() returns null and return null. (JR) + retval = nullModel; + } + } + } + if (retval == UNKNOWN) { + if (wrapper.isStrict()) { + throw new InvalidPropertyException("No such bean property: " + key); + } else { + logNoSuchKey(key, classInfo); + } + retval = wrapper.wrap(null); + } + return retval; + } catch (TemplateModelException e) { + throw e; + } catch (Exception e) { + throw new _TemplateModelException(e, + "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key), + "; see cause exception! The type of the containing value was: ", + new _DelayedFTLTypeDescription(this) + ); + } + } + + private void logNoSuchKey(String key, Map<?, ?> keyMap) { + if (LOG.isDebugEnabled()) { + LOG.debug("Key " + _StringUtil.jQuoteNoXSS(key) + " was not found on instance of " + + object.getClass().getName() + ". Introspection information for " + + "the class is: " + keyMap); + } + } + + /** + * Whether the model has a plain get(String) or get(Object) method + */ + + protected boolean hasPlainGetMethod() { + return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null; + } + + private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo) + throws IllegalAccessException, InvocationTargetException, TemplateModelException { + // See if this particular instance has a cached implementation for the requested feature descriptor + TemplateModel cachedModel; + synchronized (this) { + cachedModel = memberCache != null ? memberCache.get(desc) : null; + } + + if (cachedModel != null) { + return cachedModel; + } + + TemplateModel resultModel = UNKNOWN; + if (desc instanceof IndexedPropertyDescriptor) { + Method readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod(); + resultModel = cachedModel = + new SimpleMethodModel(object, readMethod, + ClassIntrospector.getArgTypes(classInfo, readMethod), wrapper); + } else if (desc instanceof PropertyDescriptor) { + PropertyDescriptor pd = (PropertyDescriptor) desc; + resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); + // cachedModel remains null, as we don't cache these + } else if (desc instanceof Field) { + resultModel = wrapper.wrap(((Field) desc).get(object)); + // cachedModel remains null, as we don't cache these + } else if (desc instanceof Method) { + Method method = (Method) desc; + resultModel = cachedModel = new SimpleMethodModel( + object, method, ClassIntrospector.getArgTypes(classInfo, method), wrapper); + } else if (desc instanceof OverloadedMethods) { + resultModel = cachedModel = new OverloadedMethodsModel( + object, (OverloadedMethods) desc, wrapper); + } + + // If new cachedModel was created, cache it + if (cachedModel != null) { + synchronized (this) { + if (memberCache == null) { + memberCache = new HashMap<>(); + } + memberCache.put(desc, cachedModel); + } + } + return resultModel; + } + + void clearMemberCache() { + synchronized (this) { + memberCache = null; + } + } + + protected TemplateModel invokeGenericGet(Map/*<Object, Object>*/ classInfo, Class<?> clazz, String key) + throws IllegalAccessException, InvocationTargetException, + TemplateModelException { + Method genericGet = (Method) classInfo.get(ClassIntrospector.GENERIC_GET_KEY); + if (genericGet == null) { + return UNKNOWN; + } + + return wrapper.invokeMethod(object, genericGet, new Object[] { key }); + } + + protected TemplateModel wrap(Object obj) + throws TemplateModelException { + return wrapper.getOuterIdentity().wrap(obj); + } + + protected Object unwrap(TemplateModel model) + throws TemplateModelException { + return wrapper.unwrap(model); + } + + /** + * Tells whether the model is considered to be empty. + * It is empty if the wrapped object is a 0 length {@link String}, or an empty {@link Collection} or and empty + * {@link Map}, or an {@link Iterator} that has no more items, or a {@link Boolean#FALSE}, or {@code null}. + */ + @Override + public boolean isEmpty() { + if (object instanceof String) { + return ((String) object).length() == 0; + } + if (object instanceof Collection) { + return ((Collection<?>) object).isEmpty(); + } + if (object instanceof Iterator) { + return !((Iterator<?>) object).hasNext(); + } + if (object instanceof Map) { + return ((Map<?,?>) object).isEmpty(); + } + // [FM3] Why's FALSE empty? + return object == null || Boolean.FALSE.equals(object); + } + + /** + * Returns the same as {@link #getWrappedObject()}; to ensure that, this method will be final starting from 2.4. + * This behavior of {@link BeanModel} is assumed by some FreeMarker code. + */ + @Override + public Object getAdaptedObject(Class<?> hint) { + return object; // return getWrappedObject(); starting from 2.4 + } + + @Override + public Object getWrappedObject() { + return object; + } + + @Override + public int size() { + return wrapper.getClassIntrospector().keyCount(object.getClass()); + } + + @Override + public TemplateCollectionModel keys() { + return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper)); + } + + @Override + public TemplateCollectionModel values() throws TemplateModelException { + List<Object> values = new ArrayList<>(size()); + TemplateModelIterator it = keys().iterator(); + while (it.hasNext()) { + String key = ((TemplateScalarModel) it.next()).getAsString(); + values.add(get(key)); + } + return new CollectionAndSequence(new SimpleSequence(values, wrapper)); + } + + @Override + public String toString() { + return object.toString(); + } + + /** + * Helper method to support TemplateHashModelEx. Returns the Set of + * Strings which are available via the TemplateHashModel + * interface. Subclasses that override <tt>invokeGenericGet</tt> to + * provide additional hash keys should also override this method. + */ + protected Set/*<Object>*/ keySet() { + return wrapper.getClassIntrospector().keySet(object.getClass()); + } + + @Override + public TemplateModel getAPI() throws TemplateModelException { + return wrapper.wrapAsAPI(object); + } + +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java b/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java new file mode 100644 index 0000000..7808d48 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/BeansModelCache.java @@ -0,0 +1,73 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.model.TemplateModel; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class BeansModelCache extends ModelCache { + private final Map classToFactory = new ConcurrentHashMap(); + private final Set mappedClassNames = new HashSet(); + + private final DefaultObjectWrapper wrapper; + + BeansModelCache(DefaultObjectWrapper wrapper) { + this.wrapper = wrapper; + } + + @Override + protected boolean isCacheable(Object object) { + return object.getClass() != Boolean.class; + } + + @Override + @SuppressFBWarnings(value="JLM_JSR166_UTILCONCURRENT_MONITORENTER", justification="Locks for factory creation only") + protected TemplateModel create(Object object) { + Class clazz = object.getClass(); + + ModelFactory factory = (ModelFactory) classToFactory.get(clazz); + + if (factory == null) { + // Synchronized so that we won't unnecessarily create the same factory for multiple times in parallel. + synchronized (classToFactory) { + factory = (ModelFactory) classToFactory.get(clazz); + if (factory == null) { + String className = clazz.getName(); + // clear mappings when class reloading is detected + if (!mappedClassNames.add(className)) { + classToFactory.clear(); + mappedClassNames.clear(); + mappedClassNames.add(className); + } + factory = wrapper.getModelFactory(clazz); + classToFactory.put(clazz, factory); + } + } + } + + return factory.create(object, wrapper); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java b/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java new file mode 100644 index 0000000..9db421f --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/BooleanModel.java @@ -0,0 +1,40 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import org.apache.freemarker.core.model.TemplateBooleanModel; + +/** + * <p>A class that will wrap instances of {@link java.lang.Boolean} into a + * {@link TemplateBooleanModel}. + */ +public class BooleanModel extends BeanModel implements TemplateBooleanModel { + private final boolean value; + + public BooleanModel(Boolean bool, DefaultObjectWrapper wrapper) { + super(bool, wrapper, false); + value = bool.booleanValue(); + } + + @Override + public boolean getAsBoolean() { + return value; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java b/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java new file mode 100644 index 0000000..5548796 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java @@ -0,0 +1,56 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; + +/** + * Packs a {@link Method} or {@link Constructor} together with its parameter types. The actual + * {@link Method} or {@link Constructor} is not exposed by the API, because in rare cases calling them require + * type conversion that the Java reflection API can't do, hence the developer shouldn't be tempted to call them + * directly. + */ +abstract class CallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor { + + abstract TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args) + throws TemplateModelException, InvocationTargetException, IllegalAccessException; + + abstract Object invokeConstructor(DefaultObjectWrapper ow, Object[] args) + throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, + TemplateModelException; + + abstract String getDeclaration(); + + abstract boolean isConstructor(); + + abstract boolean isStatic(); + + abstract boolean isVarargs(); + + abstract Class[] getParamTypes(); + + abstract String getName(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java b/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java new file mode 100644 index 0000000..0e31d80 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java @@ -0,0 +1,45 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import org.apache.freemarker.core.model.TemplateScalarModel; + +/** + * Represents value unwrapped both to {@link Character} and {@link String}. This is needed for unwrapped overloaded + * method parameters where both {@link Character} and {@link String} occurs on the same parameter position when the + * {@link TemplateScalarModel} to unwrapp contains a {@link String} of length 1. + */ +final class CharacterOrString { + + private final String stringValue; + + CharacterOrString(String stringValue) { + this.stringValue = stringValue; + } + + String getAsString() { + return stringValue; + } + + char getAsChar() { + return stringValue.charAt(0); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java b/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java new file mode 100644 index 0000000..8b72f2b --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java @@ -0,0 +1,148 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._ClassUtil; + +/** + * Base class for hash models keyed by Java class names. + */ +abstract class ClassBasedModelFactory implements TemplateHashModel { + private final DefaultObjectWrapper wrapper; + + private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap(); + private final Set classIntrospectionsInProgress = new HashSet(); + + protected ClassBasedModelFactory(DefaultObjectWrapper wrapper) { + this.wrapper = wrapper; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + try { + return getInternal(key); + } catch (Exception e) { + if (e instanceof TemplateModelException) { + throw (TemplateModelException) e; + } else { + throw new TemplateModelException(e); + } + } + } + + private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException { + { + TemplateModel model = (TemplateModel) cache.get(key); + if (model != null) return model; + } + + final ClassIntrospector classIntrospector; + int classIntrospectorClearingCounter; + final Object sharedLock = wrapper.getSharedIntrospectionLock(); + synchronized (sharedLock) { + TemplateModel model = (TemplateModel) cache.get(key); + if (model != null) return model; + + while (model == null + && classIntrospectionsInProgress.contains(key)) { + // Another thread is already introspecting this class; + // waiting for its result. + try { + sharedLock.wait(); + model = (TemplateModel) cache.get(key); + } catch (InterruptedException e) { + throw new RuntimeException( + "Class inrospection data lookup aborded: " + e); + } + } + if (model != null) return model; + + // This will be the thread that introspects this class. + classIntrospectionsInProgress.add(key); + + // While the classIntrospector should not be changed from another thread, badly written apps can do that, + // and it's cheap to get the classIntrospector from inside the lock here: + classIntrospector = wrapper.getClassIntrospector(); + classIntrospectorClearingCounter = classIntrospector.getClearingCounter(); + } + try { + final Class clazz = _ClassUtil.forName(key); + + // This is called so that we trigger the + // class-reloading detector. If clazz is a reloaded class, + // the wrapper will in turn call our clearCache method. + // TODO: Why do we check it now and only now? + classIntrospector.get(clazz); + + TemplateModel model = createModel(clazz); + // Warning: model will be null if the class is not good for the subclass. + // For example, EnumModels#createModel returns null if clazz is not an enum. + + if (model != null) { + synchronized (sharedLock) { + // Save it into the cache, but only if nothing relevant has changed while we were outside the lock: + if (classIntrospector == wrapper.getClassIntrospector() + && classIntrospectorClearingCounter == classIntrospector.getClearingCounter()) { + cache.put(key, model); + } + } + } + return model; + } finally { + synchronized (sharedLock) { + classIntrospectionsInProgress.remove(key); + sharedLock.notifyAll(); + } + } + } + + void clearCache() { + synchronized (wrapper.getSharedIntrospectionLock()) { + cache.clear(); + } + } + + void removeFromCache(Class clazz) { + synchronized (wrapper.getSharedIntrospectionLock()) { + cache.remove(clazz.getName()); + } + } + + @Override + public boolean isEmpty() { + return false; + } + + protected abstract TemplateModel createModel(Class clazz) + throws TemplateModelException; + + protected DefaultObjectWrapper getWrapper() { + return wrapper; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java b/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java new file mode 100644 index 0000000..ddca496 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java @@ -0,0 +1,32 @@ +/* + * 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.apache.freemarker.core.model.impl; + +/** + * Reports when the non-private interface of a class was changed to the subscribers. + */ +interface ClassChangeNotifier { + + /** + * @param classIntrospector Should only be weak-referenced from the monitor object. + */ + void subscribe(ClassIntrospector classIntrospector); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java new file mode 100644 index 0000000..4dd8931 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java @@ -0,0 +1,807 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.beans.BeanInfo; +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.model.impl.MethodAppearanceFineTuner.DecisionInput; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._NullArgumentException; +import org.slf4j.Logger; + +/** + * Returns information about a {@link Class} that's useful for FreeMarker. Encapsulates a cache for this. Thread-safe, + * doesn't even require "proper publishing" starting from 2.3.24 or Java 5. Immutable, with the exception of the + * internal caches. + * + * <p> + * Note that instances of this are cached on the level of FreeMarker's defining class loader. Hence, it must not do + * operations that depend on the Thread Context Class Loader, such as resolving class names. + */ +class ClassIntrospector { + + // Attention: This class must be thread-safe (not just after proper publishing). This is important as some of + // these are shared by many object wrappers, and concurrency related glitches due to user errors must remain + // local to the object wrappers, not corrupting the shared ClassIntrospector. + + private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER; + + private static final String JREBEL_SDK_CLASS_NAME = "org.zeroturnaround.javarebel.ClassEventListener"; + private static final String JREBEL_INTEGRATION_ERROR_MSG + = "Error initializing JRebel integration. JRebel integration disabled."; + + private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER; + static { + boolean jRebelAvailable; + try { + Class.forName(JREBEL_SDK_CLASS_NAME); + jRebelAvailable = true; + } catch (Throwable e) { + jRebelAvailable = false; + try { + if (!(e instanceof ClassNotFoundException)) { + LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e); + } + } catch (Throwable loggingE) { + // ignore + } + } + + ClassChangeNotifier classChangeNotifier; + if (jRebelAvailable) { + try { + classChangeNotifier = (ClassChangeNotifier) + Class.forName("org.apache.freemarker.core.model.impl.JRebelClassChangeNotifier").newInstance(); + } catch (Throwable e) { + classChangeNotifier = null; + try { + LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e); + } catch (Throwable loggingE) { + // ignore + } + } + } else { + classChangeNotifier = null; + } + + CLASS_CHANGE_NOTIFIER = classChangeNotifier; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Introspection info Map keys: + + /** Key in the class info Map to the Map that maps method to argument type arrays */ + private static final Object ARG_TYPES_BY_METHOD_KEY = new Object(); + /** Key in the class info Map to the object that represents the constructors (one or multiple due to overloading) */ + static final Object CONSTRUCTORS_KEY = new Object(); + /** Key in the class info Map to the get(String|Object) Method */ + static final Object GENERIC_GET_KEY = new Object(); + + // ----------------------------------------------------------------------------------------------------------------- + // Introspection configuration properties: + + // Note: These all must be *declared* final (or else synchronization is needed everywhere where they are accessed). + + final int exposureLevel; + final boolean exposeFields; + final MethodAppearanceFineTuner methodAppearanceFineTuner; + final MethodSorter methodSorter; + + /** See {@link #getHasSharedInstanceRestrictons()} */ + final private boolean hasSharedInstanceRestrictons; + + /** See {@link #isShared()} */ + final private boolean shared; + + // ----------------------------------------------------------------------------------------------------------------- + // State fields: + + private final Object sharedLock; + private final Map<Class<?>, Map<Object, Object>> cache + = new ConcurrentHashMap<>(0, 0.75f, 16); + private final Set<String> cacheClassNames = new HashSet<>(0); + private final Set<Class<?>> classIntrospectionsInProgress = new HashSet<>(0); + + private final List<WeakReference<Object/*ClassBasedModelFactory|ModelCache>*/>> modelFactories + = new LinkedList<>(); + private final ReferenceQueue<Object> modelFactoriesRefQueue = new ReferenceQueue<>(); + + private int clearingCounter; + + // ----------------------------------------------------------------------------------------------------------------- + // Instantiation: + + /** + * Creates a new instance, that is hence surely not shared (singleton) instance. + * + * @param pa + * Stores what the values of the JavaBean properties of the returned instance will be. Not {@code null}. + */ + ClassIntrospector(ClassIntrospectorBuilder pa, Object sharedLock) { + this(pa, sharedLock, false, false); + } + + /** + * @param hasSharedInstanceRestrictons + * {@code true} exactly if we are creating a new instance with {@link ClassIntrospectorBuilder}. Then + * it's {@code true} even if it won't put the instance into the cache. + */ + ClassIntrospector(ClassIntrospectorBuilder builder, Object sharedLock, + boolean hasSharedInstanceRestrictons, boolean shared) { + _NullArgumentException.check("sharedLock", sharedLock); + + exposureLevel = builder.getExposureLevel(); + exposeFields = builder.getExposeFields(); + methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner(); + methodSorter = builder.getMethodSorter(); + + this.sharedLock = sharedLock; + + this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons; + this.shared = shared; + + if (CLASS_CHANGE_NOTIFIER != null) { + CLASS_CHANGE_NOTIFIER.subscribe(this); + } + } + + /** + * Returns a {@link ClassIntrospectorBuilder}-s that could be used to create an identical {@link #ClassIntrospector} + * . The returned {@link ClassIntrospectorBuilder} can be modified without interfering with anything. + */ + ClassIntrospectorBuilder getPropertyAssignments() { + return new ClassIntrospectorBuilder(this); + } + + // ------------------------------------------------------------------------------------------------------------------ + // Introspection: + + /** + * Gets the class introspection data from {@link #cache}, automatically creating the cache entry if it's missing. + * + * @return A {@link Map} where each key is a property/method/field name (or a special {@link Object} key like + * {@link #CONSTRUCTORS_KEY}), each value is a {@link PropertyDescriptor} or {@link Method} or + * {@link OverloadedMethods} or {@link Field} (but better check the source code...). + */ + Map<Object, Object> get(Class<?> clazz) { + { + Map<Object, Object> introspData = cache.get(clazz); + if (introspData != null) return introspData; + } + + String className; + synchronized (sharedLock) { + Map<Object, Object> introspData = cache.get(clazz); + if (introspData != null) return introspData; + + className = clazz.getName(); + if (cacheClassNames.contains(className)) { + onSameNameClassesDetected(className); + } + + while (introspData == null && classIntrospectionsInProgress.contains(clazz)) { + // Another thread is already introspecting this class; + // waiting for its result. + try { + sharedLock.wait(); + introspData = cache.get(clazz); + } catch (InterruptedException e) { + throw new RuntimeException( + "Class inrospection data lookup aborded: " + e); + } + } + if (introspData != null) return introspData; + + // This will be the thread that introspects this class. + classIntrospectionsInProgress.add(clazz); + } + try { + Map<Object, Object> introspData = createClassIntrospectionData(clazz); + synchronized (sharedLock) { + cache.put(clazz, introspData); + cacheClassNames.add(className); + } + return introspData; + } finally { + synchronized (sharedLock) { + classIntrospectionsInProgress.remove(clazz); + sharedLock.notifyAll(); + } + } + } + + /** + * Creates a {@link Map} with the content as described for the return value of {@link #get(Class)}. + */ + private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) { + final Map<Object, Object> introspData = new HashMap<>(); + + if (exposeFields) { + addFieldsToClassIntrospectionData(introspData, clazz); + } + + final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz); + + addGenericGetToClassIntrospectionData(introspData, accessibleMethods); + + if (exposureLevel != DefaultObjectWrapper.EXPOSE_NOTHING) { + try { + addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods); + } catch (IntrospectionException e) { + LOG.warn("Couldn't properly perform introspection for class {}", clazz.getName(), e); + introspData.clear(); // FIXME NBC: Don't drop everything here. + } + } + + addConstructorsToClassIntrospectionData(introspData, clazz); + + if (introspData.size() > 1) { + return introspData; + } else if (introspData.size() == 0) { + return Collections.emptyMap(); + } else { // map.size() == 1 + Entry<Object, Object> e = introspData.entrySet().iterator().next(); + return Collections.singletonMap(e.getKey(), e.getValue()); + } + } + + private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz) + throws SecurityException { + for (Field field : clazz.getFields()) { + if ((field.getModifiers() & Modifier.STATIC) == 0) { + introspData.put(field.getName(), field); + } + } + } + + private void addBeanInfoToClassIntrospectionData( + Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) + throws IntrospectionException { + BeanInfo beanInfo = Introspector.getBeanInfo(clazz); + + PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors(); + if (pda != null) { + int pdaLength = pda.length; + for (int i = pdaLength - 1; i >= 0; --i) { + addPropertyDescriptorToClassIntrospectionData( + introspData, pda[i], clazz, + accessibleMethods); + } + } + + if (exposureLevel < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) { + final MethodAppearanceFineTuner.Decision decision = new MethodAppearanceFineTuner.Decision(); + DecisionInput decisionInput = null; + final MethodDescriptor[] mda = sortMethodDescriptors(beanInfo.getMethodDescriptors()); + if (mda != null) { + int mdaLength = mda.length; + for (int i = mdaLength - 1; i >= 0; --i) { + final MethodDescriptor md = mda[i]; + final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods); + if (method != null && isAllowedToExpose(method)) { + decision.setDefaults(method); + if (methodAppearanceFineTuner != null) { + if (decisionInput == null) { + decisionInput = new DecisionInput(); + } + decisionInput.setContainingClass(clazz); + decisionInput.setMethod(method); + + methodAppearanceFineTuner.process(decisionInput, decision); + } + + PropertyDescriptor propDesc = decision.getExposeAsProperty(); + if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) { + addPropertyDescriptorToClassIntrospectionData( + introspData, propDesc, clazz, accessibleMethods); + } + + String methodKey = decision.getExposeMethodAs(); + if (methodKey != null) { + Object previous = introspData.get(methodKey); + if (previous instanceof Method) { + // Overloaded method - replace Method with a OverloadedMethods + OverloadedMethods overloadedMethods = new OverloadedMethods(); + overloadedMethods.addMethod((Method) previous); + overloadedMethods.addMethod(method); + introspData.put(methodKey, overloadedMethods); + // Remove parameter type information + getArgTypesByMethod(introspData).remove(previous); + } else if (previous instanceof OverloadedMethods) { + // Already overloaded method - add new overload + ((OverloadedMethods) previous).addMethod(method); + } else if (decision.getMethodShadowsProperty() + || !(previous instanceof PropertyDescriptor)) { + // Simple method (this far) + introspData.put(methodKey, method); + getArgTypesByMethod(introspData).put(method, + method.getParameterTypes()); + } + } + } + } // for each in mda + } // if mda != null + } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY) + } + + private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData, + PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) { + if (pd instanceof IndexedPropertyDescriptor) { + IndexedPropertyDescriptor ipd = + (IndexedPropertyDescriptor) pd; + Method readMethod = ipd.getIndexedReadMethod(); + Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods); + if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) { + try { + if (readMethod != publicReadMethod) { + ipd = new IndexedPropertyDescriptor( + ipd.getName(), ipd.getReadMethod(), + null, publicReadMethod, + null); + } + introspData.put(ipd.getName(), ipd); + getArgTypesByMethod(introspData).put(publicReadMethod, publicReadMethod.getParameterTypes()); + } catch (IntrospectionException e) { + LOG.warn("Failed creating a publicly-accessible property descriptor " + + "for {} indexed property {}, read method {}", + clazz.getName(), pd.getName(), publicReadMethod, + e); + } + } + } else { + Method readMethod = pd.getReadMethod(); + Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods); + if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) { + try { + if (readMethod != publicReadMethod) { + pd = new PropertyDescriptor(pd.getName(), publicReadMethod, null); + pd.setReadMethod(publicReadMethod); + } + introspData.put(pd.getName(), pd); + } catch (IntrospectionException e) { + LOG.warn("Failed creating a publicly-accessible property descriptor " + + "for {} property {}, read method {}", + clazz.getName(), pd.getName(), publicReadMethod, + e); + } + } + } + } + + private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData, + Map<MethodSignature, List<Method>> accessibleMethods) { + Method genericGet = getFirstAccessibleMethod( + MethodSignature.GET_STRING_SIGNATURE, accessibleMethods); + if (genericGet == null) { + genericGet = getFirstAccessibleMethod( + MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods); + } + if (genericGet != null) { + introspData.put(GENERIC_GET_KEY, genericGet); + } + } + + private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData, + Class<?> clazz) { + try { + Constructor<?>[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + Constructor<?> ctor = ctors[0]; + introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes())); + } else if (ctors.length > 1) { + OverloadedMethods overloadedCtors = new OverloadedMethods(); + for (Constructor<?> ctor : ctors) { + overloadedCtors.addConstructor(ctor); + } + introspData.put(CONSTRUCTORS_KEY, overloadedCtors); + } + } catch (SecurityException e) { + LOG.warn("Can't discover constructors for class {}", clazz.getName(), e); + } + } + + /** + * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the + * class is not public, retrieves methods with same signature as its public methods from public superclasses and + * interfaces. Basically upcasts every method to the nearest accessible method. + */ + private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) { + Map<MethodSignature, List<Method>> accessibles = new HashMap<>(); + discoverAccessibleMethods(clazz, accessibles); + return accessibles; + } + + private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) { + if (Modifier.isPublic(clazz.getModifiers())) { + try { + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + MethodSignature sig = new MethodSignature(method); + // Contrary to intuition, a class can actually have several + // different methods with same signature *but* different + // return types. These can't be constructed using Java the + // language, as this is illegal on source code level, but + // the compiler can emit synthetic methods as part of + // generic type reification that will have same signature + // yet different return type than an existing explicitly + // declared method. Consider: + // public interface I<T> { T m(); } + // public class C implements I<Integer> { Integer m() { return 42; } } + // C.class will have both "Object m()" and "Integer m()" methods. + List<Method> methodList = accessibles.get(sig); + if (methodList == null) { + // TODO Collection.singletonList is more efficient, though read only. + methodList = new LinkedList<>(); + accessibles.put(sig, methodList); + } + methodList.add(method); + } + return; + } catch (SecurityException e) { + LOG.warn("Could not discover accessible methods of class {}, attemping superclasses/interfaces.", + clazz.getName(), e); + // Fall through and attempt to discover superclass/interface methods + } + } + + Class<?>[] interfaces = clazz.getInterfaces(); + for (Class<?> anInterface : interfaces) { + discoverAccessibleMethods(anInterface, accessibles); + } + Class<?> superclass = clazz.getSuperclass(); + if (superclass != null) { + discoverAccessibleMethods(superclass, accessibles); + } + } + + private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) { + if (m == null) { + return null; + } + MethodSignature sig = new MethodSignature(m); + List<Method> ams = accessibles.get(sig); + if (ams == null) { + return null; + } + for (Method am : ams) { + if (am.getReturnType() == m.getReturnType()) { + return am; + } + } + return null; + } + + private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) { + List<Method> ams = accessibles.get(sig); + if (ams == null || ams.isEmpty()) { + return null; + } + return ams.get(0); + } + + /** + * As of this writing, this is only used for testing if method order really doesn't mater. + */ + private MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors) { + return methodSorter != null ? methodSorter.sortMethodDescriptors(methodDescriptors) : methodDescriptors; + } + + boolean isAllowedToExpose(Method method) { + return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method); + } + + private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) { + @SuppressWarnings("unchecked") + Map<Method, Class<?>[]> argTypes = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY); + if (argTypes == null) { + argTypes = new HashMap<>(); + classInfo.put(ARG_TYPES_BY_METHOD_KEY, argTypes); + } + return argTypes; + } + + private static final class MethodSignature { + private static final MethodSignature GET_STRING_SIGNATURE = + new MethodSignature("get", new Class[] { String.class }); + private static final MethodSignature GET_OBJECT_SIGNATURE = + new MethodSignature("get", new Class[] { Object.class }); + + private final String name; + private final Class<?>[] args; + + private MethodSignature(String name, Class<?>[] args) { + this.name = name; + this.args = args; + } + + MethodSignature(Method method) { + this(method.getName(), method.getParameterTypes()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof MethodSignature) { + MethodSignature ms = (MethodSignature) o; + return ms.name.equals(name) && Arrays.equals(args, ms.args); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem? + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // Cache management: + + /** + * Corresponds to {@link DefaultObjectWrapper#clearClassIntrospecitonCache()}. + * + * @since 2.3.20 + */ + void clearCache() { + if (getHasSharedInstanceRestrictons()) { + throw new IllegalStateException( + "It's not allowed to clear the whole cache in a read-only " + getClass().getName() + + "instance. Use removeFromClassIntrospectionCache(String prefix) instead."); + } + forcedClearCache(); + } + + private void forcedClearCache() { + synchronized (sharedLock) { + cache.clear(); + cacheClassNames.clear(); + clearingCounter++; + + for (WeakReference<Object> regedMfREf : modelFactories) { + Object regedMf = regedMfREf.get(); + if (regedMf != null) { + if (regedMf instanceof ClassBasedModelFactory) { + ((ClassBasedModelFactory) regedMf).clearCache(); + } else if (regedMf instanceof ModelCache) { + ((ModelCache) regedMf).clearCache(); + } else { + throw new BugException(); + } + } + } + + removeClearedModelFactoryReferences(); + } + } + + /** + * Corresponds to {@link DefaultObjectWrapper#removeFromClassIntrospectionCache(Class)}. + * + * @since 2.3.20 + */ + void remove(Class<?> clazz) { + synchronized (sharedLock) { + cache.remove(clazz); + cacheClassNames.remove(clazz.getName()); + clearingCounter++; + + for (WeakReference<Object> regedMfREf : modelFactories) { + Object regedMf = regedMfREf.get(); + if (regedMf != null) { + if (regedMf instanceof ClassBasedModelFactory) { + ((ClassBasedModelFactory) regedMf).removeFromCache(clazz); + } else if (regedMf instanceof ModelCache) { + ((ModelCache) regedMf).clearCache(); // doesn't support selective clearing ATM + } else { + throw new BugException(); + } + } + } + + removeClearedModelFactoryReferences(); + } + } + + /** + * Returns the number of events so far that could make class introspection data returned earlier outdated. + */ + int getClearingCounter() { + synchronized (sharedLock) { + return clearingCounter; + } + } + + private void onSameNameClassesDetected(String className) { + // TODO: This behavior should be pluggable, as in environments where + // some classes are often reloaded or multiple versions of the + // same class is normal (OSGi), this will drop the cache contents + // too often. + LOG.info( + "Detected multiple classes with the same name, \"{}\". " + + "Assuming it was a class-reloading. Clearing class introspection caches to release old data.", + className); + forcedClearCache(); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Managing dependent objects: + + void registerModelFactory(ClassBasedModelFactory mf) { + registerModelFactory((Object) mf); + } + + void registerModelFactory(ModelCache mf) { + registerModelFactory((Object) mf); + } + + private void registerModelFactory(Object mf) { + // Note that this `synchronized (sharedLock)` is also need for the DefaultObjectWrapper constructor to work safely. + synchronized (sharedLock) { + modelFactories.add(new WeakReference<>(mf, modelFactoriesRefQueue)); + removeClearedModelFactoryReferences(); + } + } + + void unregisterModelFactory(ClassBasedModelFactory mf) { + unregisterModelFactory((Object) mf); + } + + void unregisterModelFactory(ModelCache mf) { + unregisterModelFactory((Object) mf); + } + + void unregisterModelFactory(Object mf) { + synchronized (sharedLock) { + for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) { + Object regedMf = it.next().get(); + if (regedMf == mf) { + it.remove(); + } + } + + } + } + + private void removeClearedModelFactoryReferences() { + Reference<?> cleardRef; + while ((cleardRef = modelFactoriesRefQueue.poll()) != null) { + synchronized (sharedLock) { + findClearedRef: for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) { + if (it.next() == cleardRef) { + it.remove(); + break findClearedRef; + } + } + } + } + } + + // ----------------------------------------------------------------------------------------------------------------- + // Extracting from introspection info: + + static Class<?>[] getArgTypes(Map<Object, Object> classInfo, Method method) { + @SuppressWarnings("unchecked") + Map<Method, Class<?>[]> argTypesByMethod = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY); + return argTypesByMethod.get(method); + } + + /** + * Returns the number of introspected methods/properties that should be available via the TemplateHashModel + * interface. + */ + int keyCount(Class<?> clazz) { + Map<Object, Object> map = get(clazz); + int count = map.size(); + if (map.containsKey(CONSTRUCTORS_KEY)) count--; + if (map.containsKey(GENERIC_GET_KEY)) count--; + if (map.containsKey(ARG_TYPES_BY_METHOD_KEY)) count--; + return count; + } + + /** + * Returns the Set of names of introspected methods/properties that should be available via the TemplateHashModel + * interface. + */ + Set<Object> keySet(Class<?> clazz) { + Set<Object> set = new HashSet<>(get(clazz).keySet()); + set.remove(CONSTRUCTORS_KEY); + set.remove(GENERIC_GET_KEY); + set.remove(ARG_TYPES_BY_METHOD_KEY); + return set; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Properties + + int getExposureLevel() { + return exposureLevel; + } + + boolean getExposeFields() { + return exposeFields; + } + + MethodAppearanceFineTuner getMethodAppearanceFineTuner() { + return methodAppearanceFineTuner; + } + + MethodSorter getMethodSorter() { + return methodSorter; + } + + /** + * Returns {@code true} if this instance was created with {@link ClassIntrospectorBuilder}, even if it wasn't + * actually put into the cache (as we reserve the right to do so in later versions). + */ + boolean getHasSharedInstanceRestrictons() { + return hasSharedInstanceRestrictons; + } + + /** + * Tells if this instance is (potentially) shared among {@link DefaultObjectWrapper} instances. + * + * @see #getHasSharedInstanceRestrictons() + */ + boolean isShared() { + return shared; + } + + /** + * Almost always, you want to use {@link DefaultObjectWrapper#getSharedIntrospectionLock()}, not this! The only exception is + * when you get this to set the field returned by {@link DefaultObjectWrapper#getSharedIntrospectionLock()}. + */ + Object getSharedLock() { + return sharedLock; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Monitoring: + + /** For unit testing only */ + Object[] getRegisteredModelFactoriesSnapshot() { + synchronized (sharedLock) { + return modelFactories.toArray(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java new file mode 100644 index 0000000..b17a49e --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospectorBuilder.java @@ -0,0 +1,190 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.freemarker.core.Version; +import org.apache.freemarker.core.util._NullArgumentException; + +final class ClassIntrospectorBuilder implements Cloneable { + + private static final Map/*<PropertyAssignments, Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap(); + private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue(); + + // Properties and their *defaults*: + private int exposureLevel = DefaultObjectWrapper.EXPOSE_SAFE; + private boolean exposeFields; + private MethodAppearanceFineTuner methodAppearanceFineTuner; + private MethodSorter methodSorter; + // Attention: + // - This is also used as a cache key, so non-normalized field values should be avoided. + // - If some field has a default value, it must be set until the end of the constructor. No field that has a + // default can be left unset (like null). + // - If you add a new field, review all methods in this class, also the ClassIntrospector constructor + + ClassIntrospectorBuilder(ClassIntrospector ci) { + exposureLevel = ci.exposureLevel; + exposeFields = ci.exposeFields; + methodAppearanceFineTuner = ci.methodAppearanceFineTuner; + methodSorter = ci.methodSorter; + } + + ClassIntrospectorBuilder(Version incompatibleImprovements) { + // Warning: incompatibleImprovements must not affect this object at versions increments where there's no + // change in the DefaultObjectWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react + // to some version changes that affects DefaultObjectWrapper, but not the other way around. + _NullArgumentException.check(incompatibleImprovements); + // Currently nothing depends on incompatibleImprovements + } + + @Override + protected Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Failed to clone ClassIntrospectorBuilder", e); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (exposeFields ? 1231 : 1237); + result = prime * result + exposureLevel; + result = prime * result + System.identityHashCode(methodAppearanceFineTuner); + result = prime * result + System.identityHashCode(methodSorter); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ClassIntrospectorBuilder other = (ClassIntrospectorBuilder) obj; + + if (exposeFields != other.exposeFields) return false; + if (exposureLevel != other.exposureLevel) return false; + if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false; + return methodSorter == other.methodSorter; + } + + public int getExposureLevel() { + return exposureLevel; + } + + /** See {@link DefaultObjectWrapper#setExposureLevel(int)}. */ + public void setExposureLevel(int exposureLevel) { + if (exposureLevel < DefaultObjectWrapper.EXPOSE_ALL || exposureLevel > DefaultObjectWrapper.EXPOSE_NOTHING) { + throw new IllegalArgumentException("Illegal exposure level: " + exposureLevel); + } + + this.exposureLevel = exposureLevel; + } + + public boolean getExposeFields() { + return exposeFields; + } + + /** See {@link DefaultObjectWrapper#setExposeFields(boolean)}. */ + public void setExposeFields(boolean exposeFields) { + this.exposeFields = exposeFields; + } + + public MethodAppearanceFineTuner getMethodAppearanceFineTuner() { + return methodAppearanceFineTuner; + } + + public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) { + this.methodAppearanceFineTuner = methodAppearanceFineTuner; + } + + public MethodSorter getMethodSorter() { + return methodSorter; + } + + public void setMethodSorter(MethodSorter methodSorter) { + this.methodSorter = methodSorter; + } + + private static void removeClearedReferencesFromInstanceCache() { + Reference clearedRef; + while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) { + synchronized (INSTANCE_CACHE) { + findClearedRef: for (Iterator it = INSTANCE_CACHE.values().iterator(); it.hasNext(); ) { + if (it.next() == clearedRef) { + it.remove(); + break findClearedRef; + } + } + } + } + } + + /** For unit testing only */ + static void clearInstanceCache() { + synchronized (INSTANCE_CACHE) { + INSTANCE_CACHE.clear(); + } + } + + /** For unit testing only */ + static Map getInstanceCache() { + return INSTANCE_CACHE; + } + + /** + * Returns an instance that is possibly shared (singleton). Note that this comes with its own "shared lock", + * since everyone who uses this object will have to lock with that common object. + */ + ClassIntrospector build() { + if ((methodAppearanceFineTuner == null || methodAppearanceFineTuner instanceof SingletonCustomizer) + && (methodSorter == null || methodSorter instanceof SingletonCustomizer)) { + // Instance can be cached. + ClassIntrospector instance; + synchronized (INSTANCE_CACHE) { + Reference instanceRef = (Reference) INSTANCE_CACHE.get(this); + instance = instanceRef != null ? (ClassIntrospector) instanceRef.get() : null; + if (instance == null) { + ClassIntrospectorBuilder thisClone = (ClassIntrospectorBuilder) clone(); // prevent any aliasing issues + instance = new ClassIntrospector(thisClone, new Object(), true, true); + INSTANCE_CACHE.put(thisClone, new WeakReference(instance, INSTANCE_CACHE_REF_QUEUE)); + } + } + + removeClearedReferencesFromInstanceCache(); + + return instance; + } else { + // If methodAppearanceFineTuner or methodSorter is specified and isn't marked as a singleton, the + // ClassIntrospector can't be shared/cached as those objects could contain a back-reference to the + // DefaultObjectWrapper. + return new ClassIntrospector(this, new Object(), true, false); + } + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java b/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java new file mode 100644 index 0000000..70caa66 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java @@ -0,0 +1,88 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; + +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelAdapter; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.util.UndeclaredThrowableException; + +/** + * Adapts a {@link TemplateCollectionModel} to {@link Collection}. + */ +class CollectionAdapter extends AbstractCollection implements TemplateModelAdapter { + private final DefaultObjectWrapper wrapper; + private final TemplateCollectionModel model; + + CollectionAdapter(TemplateCollectionModel model, DefaultObjectWrapper wrapper) { + this.model = model; + this.wrapper = wrapper; + } + + @Override + public TemplateModel getTemplateModel() { + return model; + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + try { + return new Iterator() { + final TemplateModelIterator i = model.iterator(); + + @Override + public boolean hasNext() { + try { + return i.hasNext(); + } catch (TemplateModelException e) { + throw new UndeclaredThrowableException(e); + } + } + + @Override + public Object next() { + try { + return wrapper.unwrap(i.next()); + } catch (TemplateModelException e) { + throw new UndeclaredThrowableException(e); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } catch (TemplateModelException e) { + throw new UndeclaredThrowableException(e); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java b/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java new file mode 100644 index 0000000..36c6947 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/CollectionModel.java @@ -0,0 +1,109 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.util.Collection; +import java.util.List; + +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.TemplateSequenceModel; + +/** + * <p>A special case of {@link BeanModel} that can wrap Java collections + * and that implements the {@link TemplateCollectionModel} in order to be usable + * in a <tt><#list></tt> block.</p> + */ +public class CollectionModel +extends + StringModel +implements + TemplateCollectionModel, + TemplateSequenceModel { + static final ModelFactory FACTORY = + new ModelFactory() + { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new CollectionModel((Collection) object, (DefaultObjectWrapper) wrapper); + } + }; + + + /** + * Creates a new model that wraps the specified collection object. + * @param collection the collection object to wrap into a model. + * @param wrapper the {@link DefaultObjectWrapper} associated with this model. + * Every model has to have an associated {@link DefaultObjectWrapper} instance. The + * model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public CollectionModel(Collection collection, DefaultObjectWrapper wrapper) { + super(collection, wrapper); + } + + /** + * Retrieves the i-th object from the collection, wrapped as a TemplateModel. + * @throws TemplateModelException if the index is out of bounds, or the + * underlying collection is not a List. + */ + @Override + public TemplateModel get(int index) + throws TemplateModelException { + // Don't forget to keep getSupportsIndexedAccess in sync with this! + if (object instanceof List) { + try { + return wrap(((List) object).get(index)); + } catch (IndexOutOfBoundsException e) { + return null; +// throw new TemplateModelException("Index out of bounds: " + index); + } + } else { + throw new TemplateModelException("Underlying collection is not a list, it's " + object.getClass().getName()); + } + } + + /** + * Tells if {@link #get(int)} will always fail for this object. + * As this object implements {@link TemplateSequenceModel}, + * {@link #get(int)} should always work, but due to a design flaw, for + * non-{@link List} wrapped objects {@link #get(int)} will always fail. + * This method exists to ease working this problem around. + * + * @since 2.3.17 + */ + public boolean getSupportsIndexedAccess() { + return object instanceof List; + } + + @Override + public TemplateModelIterator iterator() { + return new IteratorModel(((Collection) object).iterator(), wrapper); + } + + @Override + public int size() { + return ((Collection) object).size(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java b/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java new file mode 100644 index 0000000..e16e375 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/DateModel.java @@ -0,0 +1,76 @@ +/* + * 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.apache.freemarker.core.model.impl; + +import java.util.Date; + +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModel; + +/** + * Wraps arbitrary subclass of {@link java.util.Date} into a reflective model. + * Beside acting as a {@link TemplateDateModel}, you can call all Java methods + * on these objects as well. + */ +public class DateModel extends BeanModel implements + TemplateDateModel { + static final ModelFactory FACTORY = + new ModelFactory() + { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new DateModel((Date) object, (DefaultObjectWrapper) wrapper); + } + }; + + private final int type; + + /** + * Creates a new model that wraps the specified date object. + * @param date the date object to wrap into a model. + * @param wrapper the {@link DefaultObjectWrapper} associated with this model. + * Every model has to have an associated {@link DefaultObjectWrapper} instance. The + * model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public DateModel(Date date, DefaultObjectWrapper wrapper) { + super(date, wrapper); + if (date instanceof java.sql.Date) { + type = DATE; + } else if (date instanceof java.sql.Time) { + type = TIME; + } else if (date instanceof java.sql.Timestamp) { + type = DATETIME; + } else { + type = wrapper.getDefaultDateType(); + } + } + + @Override + public Date getAsDate() { + return (Date) object; + } + + @Override + public int getDateType() { + return type; + } +}
