diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala index 14ddffd646..f27087095f 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/MetaMapper.scala @@ -1090,10 +1090,11 @@ trait MetaMapper[A<:Mapper[A]] extends BaseMetaMapper with Mapper[A] { this.runSafe { logger.debug("Initializing MetaMapper for %s".format(internalTableName_$_$)) val tArray = new ListBuffer[FieldHolder] - - def isMagicObject(m: Method) = m.getReturnType.getName.endsWith("$"+m.getName+"$") && m.getParameterTypes.length == 0 - def isMappedField(m: Method) = classOf[MappedField[Nothing, A]].isAssignableFrom(m.getReturnType) def isLifecycle(m: Method) = classOf[LifecycleCallbacks].isAssignableFrom(m.getReturnType) + /* + def isMagicObject(m: Method) = m.getReturnType.getName.endsWith("$"+m.getName+"$") && m.getParameterTypes.length == 0 + //this is not currently used + //def isMappedField(m: Method) = classOf[MappedField[Nothing, A]].isAssignableFrom(m.getReturnType) import java.lang.reflect.Modifier @@ -1171,6 +1172,9 @@ trait MetaMapper[A<:Mapper[A]] extends BaseMetaMapper with Mapper[A] { } val mapperAccessMethods = findMagicFields(this, this.getClass.getSuperclass) + */ + val finder = new FieldFinder[A, MappedField[_,_]](this, logger) + val mapperAccessMethods = finder.accessorMethods mappedCallbacks = mapperAccessMethods.filter(isLifecycle).map(v => (v.getName, v)) diff --git a/persistence/mapper/src/main/scala/net/liftweb/mapper/OneToMany.scala b/persistence/mapper/src/main/scala/net/liftweb/mapper/OneToMany.scala index 76b2b80d1f..d7807a3590 100644 --- a/persistence/mapper/src/main/scala/net/liftweb/mapper/OneToMany.scala +++ b/persistence/mapper/src/main/scala/net/liftweb/mapper/OneToMany.scala @@ -20,10 +20,20 @@ package mapper /** * Add this trait to a Mapper for managed one-to-many support + * For example: class Contact extends LongKeyedMapper[Contact] with OneToMany[Long, Contact] { ... } + * @tparam K the type of the primary key + * @tparam T the mapper type * @author nafg */ trait OneToMany[K,T<:KeyedMapper[K, T]] extends KeyedMapper[K,T] { this: T => - private var oneToManyFields: List[MappedOneToManyBase[_]] = Nil + //private var oneToManyFields: List[MappedOneToManyBase[_]] = Nil + private[mapper] lazy val oneToManyFields: List[MappedOneToManyBase[_]] = { + new FieldFinder[T, MappedOneToManyBase[_]]( + getSingleton, + net.liftweb.common.Logger(classOf[OneToMany[K,T]]) + ).accessorMethods map (_.invoke(this).asInstanceOf[MappedOneToManyBase[_]]) + } + /** * An override for save to propagate the save to all children * of this parent. @@ -100,7 +110,7 @@ trait OneToMany[K,T<:KeyedMapper[K, T]] extends KeyedMapper[K,T] { this: T => } protected def delegate_=(d: List[O]) = _delegate = d - oneToManyFields = this :: oneToManyFields + //oneToManyFields = this :: oneToManyFields /** * Takes ownership of e. Sets e's foreign key to our primary key @@ -282,7 +292,8 @@ class LongMappedMapper[T<:Mapper[T], O<:KeyedMapper[Long,O]](theOwner: T, foreig * @deprecated Use LongMappedMapper instead, to avoid mixing in MappedLongForeignKey manually. May be folded into it in the future. * @author nafg */ -@deprecated +//TODO fold into LongMappedMapper +@deprecated("Use LongMappedMapper or LongMappedForeignLey instead") trait LongMappedForeignMapper[T<:Mapper[T],O<:KeyedMapper[Long,O]] extends MappedLongForeignKey[T,O] with LifecycleCallbacks { @@ -316,3 +327,93 @@ trait LongMappedForeignMapper[T<:Mapper[T],O<:KeyedMapper[Long,O]] else Nil } +class FieldFinder[A <: Mapper[A], T: ClassManifest](metaMapper: Mapper[A], logger: net.liftweb.common.Logger) { + import java.lang.reflect._ + + logger.debug("Created FieldFinder for " + classManifest[T].erasure) + + def isMagicObject(m: Method) = m.getReturnType.getName.endsWith("$"+m.getName+"$") && m.getParameterTypes.length == 0 + + def typeFilter: Class[_]=>Boolean = classManifest[T].erasure.isAssignableFrom + + /** + * Find the magic mapper fields on the superclass + */ + def findMagicFields(onMagic: Mapper[A], staringClass: Class[_]): List[Method] = { + // If a class name ends in $module, it's a subclass created for scala object instances + def deMod(in: String): String = + if (in.endsWith("$module")) in.substring(0, in.length - 7) + else in + + // find the magic fields for the given superclass + def findForClass(clz: Class[_]): List[Method] = clz match { + case null => Nil + case c => + // get the names of fields that represent the type we want + + val fields = Map(c.getDeclaredFields. + filter{f => + val ret = typeFilter(f.getType) + logger.debug("typeFilter(" + f.getType + "); T=" + classManifest[T].erasure) + ret + }. + map(f => (deMod(f.getName), f)) :_*) + + logger.debug("fields: " + fields) + + // this method will find all the super classes and super-interfaces + def getAllSupers(clz: Class[_]): List[Class[_]] = clz match { + case null => Nil + case c => + c :: c.getInterfaces.toList.flatMap(getAllSupers) ::: + getAllSupers(c.getSuperclass) + } + + // does the method return an actual instance of an actual class that's + // associated with this Mapper class + def validActualType(meth: Method): Boolean = { + try { + // invoke the method + meth.invoke(onMagic) match { + case null => + logger.debug("Not a valid mapped field: %s".format(meth.getName)) + false + case inst => + // do we get a T of some sort back? + if (!typeFilter(inst.getClass)) false + else { + // find out if the class name of the actual thing starts + // with the name of this class or some superclass... + // basically, is an inner class of this class + getAllSupers(clz).find{ + c => + inst.getClass.getName.startsWith(c.getName)}.isDefined + } + } + + } catch { + case e => + logger.debug("Not a valid mapped field: %s, got exception: %s".format(meth.getName, e)) + false + } + } + + // find all the declared methods + val meths = c.getDeclaredMethods.toList. + filter(_.getParameterTypes.length == 0). // that take no parameters + filter(m => Modifier.isPublic(m.getModifiers)). // that are public + filter(m => fields.contains(m.getName) && // that are associated with private fields + fields(m.getName).getType == m.getReturnType). + filter(validActualType) // and have a validated type + + meths ::: findForClass(clz.getSuperclass) + } + + findForClass(staringClass).distinct + } + + lazy val accessorMethods = findMagicFields(metaMapper, metaMapper.getClass.getSuperclass) + } + + + diff --git a/persistence/mapper/src/test/scala/net/liftweb/mapper/OneToManySpecs.scala b/persistence/mapper/src/test/scala/net/liftweb/mapper/OneToManySpecs.scala new file mode 100644 index 0000000000..377568bb74 --- /dev/null +++ b/persistence/mapper/src/test/scala/net/liftweb/mapper/OneToManySpecs.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2010 WorldWide Conferencing, LLC + * + * 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 net.liftweb { +package mapper { + +import org.specs._ +import _root_.org.specs.runner.{JUnit3, ConsoleRunner} + +class OneToManySpecsAsTest extends JUnit3(OneToManySpecs) +object OneToManySpecsRunner extends ConsoleRunner(OneToManySpecs) + +object OneToManySpecs extends Specification { + + val provider = DbProviders.H2MemoryProvider + + private def ignoreLogger(f: => AnyRef): Unit = () + def setupDB { + MapperRules.createForeignKeys_? = c => false + provider.setupDB + Schemifier.destroyTables_!!(ignoreLogger _, Contact, Phone) + Schemifier.schemify(true, ignoreLogger _, Contact, Phone) + } + + "OneToMany" should { + "detect all MappedOneToMany fields" in { + setupDB + val contact = Contact.create + val fields = contact.oneToManyFields + fields.length must_== 1 + fields(0).asInstanceOf[Any] must_== contact.phones + } + } + +} + + + +class Contact extends LongKeyedMapper[Contact] with IdPK with OneToMany[Long, Contact] { + def getSingleton = Contact + object phones extends MappedOneToMany(Phone, Phone.contact) +} +object Contact extends Contact with LongKeyedMetaMapper[Contact] + +class Phone extends LongKeyedMapper[Phone] with IdPK { + def getSingleton = Phone + object contact extends LongMappedMapper(this, Contact) + object number extends MappedString(this, 10) +} +object Phone extends Phone with LongKeyedMetaMapper[Phone] + + +} +}