PageRenderTime 46ms CodeModel.GetById 30ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/scalate-core/src/main/scala/org/fusesource/scalate/introspector/Introspector.scala

http://github.com/scalate/scalate
Scala | 273 lines | 150 code | 58 blank | 65 comment | 24 complexity | cdd6232dd92d7f8ba1fb4631be22bfe4 MD5 | raw file
  1/**
  2 * Copyright (C) 2009-2011 the original author or authors.
  3 * See the notice.md file distributed with this work for additional
  4 * information regarding copyright ownership.
  5 *
  6 * Licensed under the Apache License, Version 2.0 (the "License");
  7 * you may not use this file except in compliance with the License.
  8 * You may obtain a copy of the License at
  9 *
 10 *     http://www.apache.org/licenses/LICENSE-2.0
 11 *
 12 * Unless required by applicable law or agreed to in writing, software
 13 * distributed under the License is distributed on an "AS IS" BASIS,
 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15 * See the License for the specific language governing permissions and
 16 * limitations under the License.
 17 */
 18package org.fusesource.scalate.introspector
 19
 20import java.beans.{ PropertyDescriptor, Introspector => BeanInt }
 21import java.util.concurrent.locks.ReentrantReadWriteLock
 22import org.fusesource.scalate.util.ProductReflector
 23import collection.mutable.{ HashMap, Map, WeakHashMap }
 24import java.lang.reflect.{ Modifier, Method }
 25
 26object Introspector {
 27
 28  private[this] val rwl = new ReentrantReadWriteLock()
 29  private[this] val rlock = rwl.readLock
 30  private[this] val wlock = rwl.writeLock
 31
 32  /**
 33   * The global caching strategy for introspection which by default uses a weak hash map
 34   * to avoid keeping around cached data for classes which are garbage collected
 35   */
 36  private[this] val cache = WeakHashMap.empty[Class[_], Introspector[_]]
 37
 38  /**
 39   * Returns the Introspector for the given type using the current cache if it is defined
 40   */
 41  def apply[T](aType: Class[T]): Introspector[T] = {
 42    safeGetOrElseUpdate(aType).asInstanceOf[Introspector[T]]
 43  }
 44
 45  /**
 46   * Does thread-safe access and modification of the cache map using read and write locks
 47   */
 48  private def safeGetOrElseUpdate[T](key: Class[T]): Introspector[_] = {
 49    def get(): Option[Introspector[_]] = {
 50      rlock.lock
 51      try {
 52        cache.get(key).filterNot(_ == null)
 53      } finally rlock.unlock
 54    }
 55    def update(): Introspector[_] = {
 56      wlock.lock
 57      try {
 58        get() getOrElse {
 59          cache.remove(key)
 60          val d = createIntrospector(key)
 61          cache.put(key, d)
 62          d
 63        }
 64      } finally wlock.unlock
 65    }
 66    get().getOrElse(update())
 67  }
 68
 69  /**
 70   * Creates a new introspector
 71   */
 72  def createIntrospector(aType: Class[_]): Introspector[_] = {
 73    if (classOf[Product].isAssignableFrom(aType)) {
 74      new ProductIntrospector(aType)
 75    } else {
 76      new BeanIntrospector(aType)
 77    }
 78  }
 79}
 80
 81/**
 82 * @version $Revision : 1.1 $
 83 */
 84trait Introspector[T] {
 85  private[this] lazy val _expressions = createExpressions
 86
 87  def elementType: Class[T]
 88
 89  /**
 90   * Returns the CSS style name of the introspection type
 91   */
 92  def typeStyleName: String = decapitalize(elementType.getSimpleName)
 93
 94  def properties: collection.Seq[Property[T]]
 95
 96  lazy val propertyMap = Map[String, Property[T]](properties.map(p => p.name -> p).toSeq: _*)
 97
 98  def property(name: String): Option[Property[T]] = propertyMap.get(name) match {
 99    case s: Some[Property[T]] => s
100    case _ =>
101      // lets allow bad case to find the property if it finds exactly one match
102      val found = properties.filter(_.name.equalsIgnoreCase(name))
103      if (found.size == 1) {
104        Some(found.head)
105      } else {
106        None
107      }
108  }
109
110  /**
111   * Returns the value by name on the given instance.
112   *
113   * Typically this method returns the named property or returns the method with no parameters.
114   * If no property or method with zero arguments is available then a suitable function is searched for
115   * such as a function which takes a String parameter if using higher order functions or tags on objects.
116   */
117  def get(name: String, instance: T): Option[Any] = {
118    expressions.get(name) match {
119      case Some(e: Expression[T]) =>
120        Some(e.evaluate(instance))
121      case _ => None
122    }
123  }
124
125  /**
126   * Returns all the expressions available for the given type
127   */
128  def expressions: Map[String, Expression[T]] = _expressions
129
130  protected def createExpressions: Map[String, Expression[T]] = {
131    val answer = new HashMap[String, Expression[T]]
132    for (p <- properties) {
133      answer += p.name -> p
134    }
135
136    /**
137     * Lazily create and add the given property using the method name
138     * and the bean property style name of the method if it has not already
139     * been added
140     */
141    def add(method: Method, property: => Property[T]): Unit = {
142      val name = method.getName
143      answer.getOrElseUpdate(name, property)
144      if (name.matches("get\\p{javaUpperCase}.*")) {
145        val propertyName = decapitalize(name.substring(3))
146        answer.getOrElseUpdate(propertyName, property)
147      }
148    }
149
150    val nonVoidPublicMethods = elementType.getMethods.filter(m => Modifier.isPublic(m.getModifiers) && isValidReturnType(m.getReturnType))
151
152    // include all methods which have no arguments
153    for (m <- nonVoidPublicMethods if m.getParameterTypes.isEmpty) {
154      add(m, new MethodProperty(m))
155    }
156
157    // include all methods which have a single String parameter
158    for (m <- nonVoidPublicMethods if isSingleParameterOfType(m, classOf[String])) {
159      add(m, new StringFunctorProperty(m))
160    }
161    answer
162  }
163
164  protected def isSingleParameterOfType(method: Method, paramType: Class[_]): Boolean = {
165    val types = method.getParameterTypes
166    types.size == 1 && paramType.isAssignableFrom(types(0))
167  }
168
169  protected def decapitalize(name: String): String = BeanInt.decapitalize(name)
170
171  protected def isValidReturnType(clazz: Class[_]): Boolean = clazz != classOf[Void] && clazz != Void.TYPE
172}
173
174trait Expression[T] {
175  def evaluate(instance: T): Any
176}
177
178trait Property[T] extends Expression[T] {
179  def name: String
180
181  def propertyType: Class[_]
182
183  def label: String
184
185  def description: String
186
187  def readOnly: Boolean
188
189  def optional: Boolean
190
191  def apply(instance: T): Any = evaluate(instance)
192
193  def evaluate(instance: T): Any
194
195  def set(instance: T, value: Any): Unit
196}
197
198class BeanIntrospector[T](val elementType: Class[T]) extends Introspector[T] {
199  val beanInfo = BeanInt.getBeanInfo(elementType)
200  val _properties = beanInfo.getPropertyDescriptors.filter(p => p.getReadMethod != null && p.getName != "class").map(createProperty(_))
201
202  def properties = {
203    _properties
204  }
205
206  protected def createProperty(descriptor: PropertyDescriptor) = new BeanProperty[T](descriptor)
207}
208
209/**
210 * A property for a Java Bean property
211 */
212case class BeanProperty[T](descriptor: PropertyDescriptor) extends Property[T] {
213  def name = descriptor.getName
214
215  def propertyType = descriptor.getPropertyType
216
217  def readOnly = descriptor.getWriteMethod == null
218
219  def optional = descriptor.getWriteMethod != null && !propertyType.isPrimitive
220
221  // TODO use annotations to find description / label?
222  def label = descriptor.getDisplayName
223
224  def description = descriptor.getShortDescription
225
226  def evaluate(instance: T) = descriptor.getReadMethod.invoke(instance)
227
228  def set(instance: T, value: Any) = descriptor.getWriteMethod.invoke(instance, value.asInstanceOf[AnyRef])
229
230  override def toString = "BeanProperty(" + name + ": " + propertyType.getName + ")"
231}
232
233class ProductIntrospector[T](val elementType: Class[T]) extends Introspector[T] {
234  def properties = ProductReflector.accessorMethods(elementType).map(createProperty(_))
235
236  protected def createProperty(method: Method) = new MethodProperty[T](method)
237}
238
239/**
240 * A property which just maps to a method with no arguments
241 */
242class MethodProperty[T](method: Method) extends Property[T] {
243  def name = method.getName
244
245  def propertyType = method.getReturnType
246
247  def readOnly = true
248
249  def optional = false
250
251  def label = name
252
253  def description = name
254
255  def evaluate(instance: T) = method.invoke(instance)
256
257  def set(instance: T, value: Any) = throw new UnsupportedOperationException("Cannot set " + this)
258
259  override def toString = "MethodProperty(" + name + ": " + propertyType.getName + ")"
260}
261
262/**
263 * A property which returns a Function which when invoked it invokes the underlying
264 * method on the given object
265 */
266class StringFunctorProperty[T](method: Method) extends MethodProperty[T](method) {
267  override def evaluate(instance: T) = {
268    def f(arg: String) = method.invoke(instance, arg)
269    f _
270  }
271
272  override def toString = "StringFunctorProperty(" + name + ")"
273}