/* * Copyright 2011 JBoss Inc * * 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 org.drools.semantics.trait.java.model; import junit.framework.Assert; import org.drools.KnowledgeBase; import org.drools.KnowledgeBaseFactory; import org.drools.builder.KnowledgeBuilder; import org.drools.builder.KnowledgeBuilderFactory; import org.drools.builder.ResourceType; import org.drools.io.impl.ClassPathResource; import org.drools.runtime.StatefulKnowledgeSession; import org.drools.semantics.traits.java.IThing; import org.drools.semantics.traits.java.TraitMantle; import org.junit.Test; import org.drools.semantics.trait.java.model.domain.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; public class TestTraits { @Test /** * In this test, a dynamic Trait is applied to an Object * * A dynamic Trait is * - an interface * - actually, a partially implemented interface * - applied to an INSTANCE * - which can be removed as necessary * * When an object wears a "Trait", it is expected to, and thus does, * exhibit some properties (fields) and behaviour (methods). * These additional features might not be exposed/available * in the original interface the object was implementing. * * A "Trait" is then composed by an interface (aka "Role") * and an optional implementing Strategy. * -- The Role adds to the object's interface * The wrapped object might actually implement some of * the methods required by the Role * -- IF NOT, the Strategy is used to fill in any missing * behaviour * * So, the entities involved: * -- The core object * ---- and its initial interface, if any * -- The Trait * ---- the Role interface * ---- the Strategy implementation * * In this implementation, Roles add on top of the existing * core object interface, instead of hiding it. This also * means that several traits can be added to the same * core object at the same time. * * Notice that, in practice, this implies MULTIPLE INHERITANCE, * with all related problems, as two traits might add the * same method (i.e. name and signature) * This problem is solved taking inspiration from Scala: * in case of ambiguities, the last trait applied determines * which strategy method will be eventually called. * Notice that a trait can be "reapplied" to an object * already having that trait. * * The trait approach, useful when methods are involved, * has also been enhanced for dynamic properties (fields). * In canonical traits, additional properties are expected * to be supplied by the Strategy implementors. * Here, a traited object is automatically given a property * Map<String,Objet> which is automatically used * to implement getters/setters defined by Roles but * for some reason not provided by either the core object * or any Strategy implementor. * * This map can also be accessed directly, to add/access * dynamic properties even when they're not part of a formal * interface. * */ public void testTrait() { Object o = new Object(); Map<String, Object> m = new HashMap<String,Object>(); m.put("echoDynaProp","echo"); IEcho echo = TraitMantle.wrap(o, m, IEcho.class); // this method is provided by the echo trait String rev = echo.echo("Hail Word"); assertEquals("droW liaH", rev); // this is a property provided by the echo trait assertEquals("droW liaH", echo.getLastEchoMessage()); // this is a dynamic property exposed by the Role // and provided using the dynamic map assertEquals("echo", echo.getEchoDynaProp()); } @Test /** * In order to define a Trait, three entities need to be defined, * bottom up * * - A Trait Strategy implementor * --- could likely be a (stateless) singleton * - A Trait interface which the implementor implements. * - A Role interface extending the Trait interface * * ( roughly speaking, the Role interface exposes methods/properties * in the inner object; the Trait interface adds methods; the * implementor and the dynamic map provide any missing implementation ) * * Another reason for having two interfaces is, the Role interface * usually has a clear name, domain dependent. So, the Role * is applied when some logic (or the programmer) decides that an * object can have that role and behave accordingly. * * * This becomes interesting when we have several dynamic properties, * e.g. provided as triples, and we want to run a recognition * algorithm. Assuming that the Roles are (automatically) derived * from the concepts defined in an ontology... * * Out-of-the box, we provide the IThing interface, which is a Trait. * When a trait is applied to an object, the resulting object can * be cast to the Trait Role interface and used as such, but that's * where the story ends, i.e. the Role+Trait interfaces define the * (new) methods that can be used. * * When the IThing trait is applied, additional meta-traiting power * is provided. IThing allows multiple traits to be applied to the * same object at the same time, move from one trait to the other * (i.e. "rewiring" to avoid the multiple inheritance problem, * while keeping all trait interfaces), and access the dynamic properties. * * All "semantic" (i.e. concept-defined) traits naturally extend * the IThing interface, though nothing prevents user-defined traits * from doing so. * Notice, then, that if you apply a "semantic" trait, you also * provide the IThing trait. * (impl. note : the IThing just EXPOSES some internal functionalities * which are added whenever any trait is applied) */ public void testSemanticTrait() { Object o = new Object(); Map<String, Object> m = new HashMap<String,Object>(); m.put("school","Zkool"); // First, the meta-trait IThing is applied. // Supports meta-traiting operations, but does // not define any domain-specific behaviour IThing<Object> thing = TraitMantle.wrap(o, m); assertFalse(thing instanceof IStudent); // Now something allows as to infer that our thing // was actually a student: IStudent s = thing.don(IStudent.class); // the "traited" reference we got back has the original object at its core assertEquals(o, s.getCore()); // is effectively an instance of the interface we provided assertTrue(s instanceof IStudent); // and the type can be queried explicitly assertTrue(thing.hasType(IStudent.class)); // but now, we have type and methods! assertEquals(IBreath_Impl.BREATHE,s.breathe()); assertEquals("Zkool",s.getSchool()); } @Test /** * This test looks at what happens to the core object when * it's actually a real ("legacy") object with fields and methods * * Semantic traits inheriting from IThing are actually generic, * so you can provide the core type at traiting time. * * The effects are twofold: * - you can call getCore() on any traited reference and * get a typed reference to the core object * - if the core object implemented any interface, those * interfaces are still implemented by the traited reference, * and calls are redirected on the core object * (** note: traits have precedence on methods, so they could override methods ) * (** TODO: check what to do with properties ) * */ public void testCore() { Human h = new Human("john",23); // now we have typed traits IThing<Human> thing = TraitMantle.wrap(h); assertFalse(thing instanceof IStudent); IStudent<Human> humanStudent = thing.don(IStudent.class); // core is typed, so you can invoke methods String ans = humanStudent.getCore().humanSkill(); assertEquals("humanSkill",ans); // core had an interface, so you can cast // and call methods from that interface directly on the traited reference String ans2 = ((IHuman) thing).humanSkill(); assertEquals("humanSkill",ans2); // methods not exposed through an interface can still be called // on core explicitly String ans3 = humanStudent.getCore().pureHumanSkill(); assertEquals("pureHumanSkill",ans3); } @Test /** * Traits can be stacked, and applied in any sequence, * using the IThing interface. * * After a core object has been wrapped, it can * don additional traits. * * Notice that if you wrapped the object again, * you would get a separate traited entity from * the perspective of implemented interfaces. * * However, if you wrap the SAME object, the * underlying data structure is the same * BUT the dynamic properties are not! * * (not a strict requirement, the reason is * the dynamic property map is copied, not referenced) * */ public void testMultiTraiting() { Object o = new Object(); Map<String, Object> m = new HashMap<String, Object>(); m.put("wage",22.0); // start from thing IThing<Object> thing = TraitMantle.wrap(o, m); // now o is ALSO a student IStudent<Object> s = thing.don(IStudent.class); assertTrue(s instanceof IThing); assertTrue(s instanceof IStudent); assertFalse(s instanceof IWorker); // now o is ALSO a worker IWorker<Object> w = s.don(IWorker.class); assertTrue(w instanceof IThing); assertTrue(w instanceof IWorker); assertTrue(w instanceof IStudent); // so one can work with it as if it were // a worker w.setWage(33.0); assertEquals(33.0,w.getWage()); // or a student, just cast... assertEquals(null,((IStudent) w).getSchool()); // Now o is wrapped again, clean of traits IThing<Object> thing2 = TraitMantle.wrap(o,m); // even if the core is the same assertEquals(thing.getCore(),thing2.getCore()); // traiting needs to be applied again assertFalse(thing2 instanceof IWorker); assertFalse(thing2 instanceof IStudent); // and dynamic properties changed on "thing" // are not propagated to "thing2" IWorker<Object> w2 = thing2.don(IWorker.class); assertEquals(22.0,w2.getWage()); // but you can do THIS... // after all, thing is a Map... IThing<Object> thing3 = TraitMantle.wrap(o,thing); assertEquals(33.0,thing3.don(IWorker.class).getWage()); } @Test /** * For meta-reasoning purposes, types can be accessed explicitly * using the IThing interface or any of its sub-interfaces * * There's also a caveat * IThing thing = wrap(o,m); * IStudent s = thing.don(IStudent); * * When the wrapper referred by "thing" is created, the trait * IThing is added to o. * Later, the trait IStudent is also added. * * Problem is, by the time "thing" is set, a facade proxy is created * which is only aware of the IThing interface that the traited object * has been given. * When "s" is created, it is a proxy with both interfaces. * * Both proxies refer to the same wrapper for the core object o, * so you can call getType() on both thing and s and get the same * correct result. * But, proxy references might not be updated, so beware when * calling instanceof! * * So, it is good practice to always use the latest reference * returned by a "don" operation * */ public void testThingDon() { Object o = new Object(); Map<String, Object> m = new HashMap<String,Object>(); IThing<Object> thing = TraitMantle.wrap(o, m); assertFalse(thing instanceof IStudent); IStudent<Object> s = thing.don(IStudent.class); IWorker<Object> w = s.don(IWorker.class); IStudent<Object> s2 = w.don(IStudent.class); assertTrue(thing.hasType(IStudent.class)); assertTrue(thing.hasType(IWorker.class)); assertTrue(thing.hasType(IThing.class)); assertTrue(s.hasType(IStudent.class)); assertTrue(s.hasType(IWorker.class)); assertTrue(s.hasType(IThing.class)); assertTrue(w.hasType(IStudent.class)); assertTrue(w.hasType(IWorker.class)); assertTrue(w.hasType(IThing.class)); assertTrue(s2.hasType(IStudent.class)); assertTrue(s2.hasType(IWorker.class)); assertTrue(s2.hasType(IThing.class)); // this shows the "out-of-date" proxy "problem" // solution : use the latest reference assertTrue(thing instanceof IThing); assertFalse(thing instanceof IWorker); assertFalse(thing instanceof IStudent); assertTrue(s2 instanceof IThing); assertTrue(s2 instanceof IWorker); assertTrue(s2 instanceof IStudent); } @Test /** * Traits can be removed. * Should an object no longer have the requirements to qualify for - * or the reasons for exposing - a trait, the trait can be removed * (but applied again later, just beware of any state kept in the implementor!) * * When a trait is shed, the returning interface is IThing. * Notice that IThing can't be shed. * * Also notice that the "shed" feature is available only if you * apply a Trait that extends the IThing interface * */ public void testThingShed() { Object o = new Object(); Map<String, Object> m = new HashMap<String,Object>(); IThing<Object> thing = TraitMantle.wrap(o, m); assertFalse(thing instanceof IStudent); thing = thing.don(IStudent.class); thing = thing.don(IWorker.class); assertTrue(thing instanceof IStudent); assertTrue(thing instanceof IWorker); assertTrue(thing instanceof IThing); thing = thing.shed(IStudent.class); assertFalse(thing.hasType(IStudent.class)); assertFalse(thing instanceof IStudent); thing = thing.don(IStudent.class); assertTrue(thing instanceof IStudent); IStudent<Object> s2 = (IStudent) thing; assertNotNull(s2); // even if you apply a specific trait directly, // the IThing trait is implicitly applied IWorker worker2 = TraitMantle.wrap(o,m,IWorker.class); IThing thing2 = worker2.shed(IWorker.class); assertTrue(thing2.hasType(IThing.class)); // by choice, no exception is raised, simply nothing happens thing2 = thing2.shed(IThing.class); assertTrue(thing2.hasType(IThing.class)); } @Test /** * Remark: ensure that traited facades can share the same core object */ public void testThingCore() { Object o = new Object(); Object o2 = new Object(); Map<String, Object> m = new HashMap<String,Object>(); Map<String, Object> m2 = new HashMap<String,Object>(); IThing<Object> thing = TraitMantle.wrap(o, m); IThing<Object> thing2 = TraitMantle.wrap(o2, m2); assertFalse(thing.getCore().equals(thing2.getCore())); IStudent<Object> s = thing.don(IStudent.class); IWorker<Object> w = s.don(IWorker.class); IStudent<Object> s2 = w.don(IStudent.class); IWorker<Object> w2 = thing2.don(IWorker.class); assertEquals(o,s.getCore()); assertEquals(o,w.getCore()); assertEquals(o,s2.getCore()); assertEquals(o,thing.getCore()); assertEquals(o2,w2.getCore()); assertEquals(o2,thing2.getCore()); } @Test /** * IThing extends the Map interface to ensure that you * can add "dynamic" properties, which are not * explicitly included in any interface (using getters/setters). * * The obvious intent is: (i) enhance an object with dynamic * properties; (ii) classify/recognize it according to those * properties; (iii) wear any trait that apply and allow * that object to benefit from the use strong typing - * at least until that trait is shed.... * */ public void testThingAsMap() { Object o = new Object(); Map<String, Object> m = new HashMap<String,Object>(); m.put("age",18); m.put("name","jon"); m.put("weight",84.23); IThing<Object> thing = TraitMantle.wrap(o, m); assertEquals("jon",(String) thing.get("name")); // notice that unlike static properties, dynamic properties can be removed // which is different from making them null! Integer i = (Integer) thing.remove("age"); assertEquals(18, i.intValue()); assertTrue(thing.containsKey("weight")); assertEquals(84.23, (Double) thing.get("weight"), 1e-5); } @Test /** * ... notice that while a trait is being used, which * relies on dynamic properties, getters and setters * from the trait interface will target the dynamic * fields in the property map. * The map interface will still apply, so both * options are available * * Moreover, the other way around is provided! * If a getter/setter from an interface is not supported by any * implementor, the property will be created as a dynamic one */ public void testTwinAccess() { Human h = new Human("jon",18); Map<String,Object> m = new HashMap<String, Object>(); m.put("school","Skool"); IStudent<Human> pers = TraitMantle.wrap(h, m, IStudent.class); assertEquals("jon",pers.get("name")); assertEquals("jon",pers.getName()); assertEquals("Skool",pers.get("school")); assertEquals("Skool",pers.getSchool()); // property from IPerson, hooked to the "name" field of Human pers.setName("adam"); assertEquals("adam",pers.get("name")); assertEquals("adam", pers.getName()); pers.set("name", "bob"); assertEquals("bob",pers.get("name")); assertEquals("bob",pers.getName()); pers.put("name","charles"); assertEquals("charles", pers.get("name")); assertEquals("charles",pers.getName()); // property from IStudent, hooked to a dynamic property pers.setSchool("s1"); assertEquals("s1",pers.get("school")); assertEquals("s1",pers.getSchool()); pers.set("school","s2"); assertEquals("s2",pers.get("school")); assertEquals("s2",pers.getSchool()); pers.put("school","s3"); assertEquals("s3",pers.get("school")); assertEquals("s3",pers.getSchool()); } @Test /** * When multiple traits are applied, with multiple implementors, * the implementors are kept in a stack. * Currently, method implementation are being searched in that * stack, starting from the top. * * To reduce inefficiency, a trait can be reapplied to * force the trait implementor to appear on top of the stack */ public void testMultiTraitImpl() { Object o = new Object(); IThing<Object> thing = TraitMantle.wrap(o); thing.don(IStudent.class); IWorker<Object> w = thing.don(IWorker.class); IStudent<Object> s = (IStudent) w; System.out.println(w.toil()); // not so efficient (should don the student's mantle again) // but workable as there's no overlapping System.out.println(s.breathe()); } @Test /** * The reason for that choice is: avoid the "diamond problem" * when multiple inheritance is present. * * Here, IPersons, IStudents and IWorkers can "sum" integers * with different results * (IPersons always return 0, IWorkers give a wrong result by +1, * IStudents return the correct sum) * * As traits ovverride each other, the last trait implementor providing the * desired method will actually be executed. * (re)-wiring a trait causes its implementor to appear on top of * the stack */ public void testTraitOverriding() { Human h = new Human("john", 18); IThing<Human> thing = TraitMantle.wrap(h); System.out.println(thing.getTypes()); IWorker<Human> fw = thing.don(IWorker.class); System.out.println(fw.getTypes()); fw.sum(3,2); assertEquals(6, fw.sum(3, 2)); IPerson<Human> fp = thing.don(IPerson.class); System.out.println(fp.getTypes()); fp.sum(3,2); fw.sum(3,2); assertEquals(0,fp.sum(3,2)); assertEquals(0,fw.sum(3,2)); IPerson<Human> fs = thing.don(IStudent.class); System.out.println(fs.getTypes()); fs.sum(3,2); assertEquals(5,fs.sum(3,2)); IWorker<Human> full = thing.don(IWorker.class); System.out.println(full.getTypes()); full.sum(3,2); assertEquals(6, full.sum(3, 2)); } @Test /** * full, legacy example mimicking a semantic recognition problem, prior to using a proper trait interface * better expressed in the next, rule-based test. */ public void testTraits() { Human p1 = new Human("john",18); Triple[] tripples = new Triple[] { new Triple( p1, "weight", 90.0 ), new Triple( p1, "height", 180.0 ), new Triple( p1, "wage", 43.72) // ........... }; Map<String,Object> props1 = new HashMap<String,Object>(); for (Triple t : tripples) { if ( t.subject.equals( p1 ) ) { props1.put(t.property,t.object); } } /*---------------------------------------------------------------------*/ // Human entity enhanced with dynamic properties IThing<Human> thing = TraitMantle.wrap(p1, props1); // hard-classified as a person IPerson<Human> p1AsPerson = thing.don(IPerson.class); // almost-hard-classified as a student if ( p1AsPerson.getAge() < 30 ) { IStudent<Human> p1AsStudent = p1AsPerson.don(IStudent.class); p1AsStudent.setSchool("skool"); p1AsStudent.setName("johnnie"); assertEquals("johnnie", p1AsStudent.getName()); assertEquals("skool", p1AsStudent.getSchool()); } // and a worker as well if ( p1AsPerson.getAge() >= 18 ) { IWorker<Human> p1AsWorker = p1AsPerson.don(IWorker.class); assertEquals(180.0, p1AsWorker.getHeight(), 1e-16); IStudent<Human> p1AsStudentAgain = p1AsWorker.don(IStudent.class); assertEquals("skool", p1AsStudentAgain.getSchool()); } // do something IStudent<Human> p1AsStudentOnceMore = p1AsPerson.don(IStudent.class); p1AsStudentOnceMore.setAge(33); // then another recognition process decidedes that it's no longer a worker if ( p1AsPerson.getAge() > 25 ) { IPerson<Human> p = p1AsStudentOnceMore.don(IPerson.class); IThing<Human> x = p.shed(IWorker.class); assertFalse(p.hasType(IWorker.class)); assertFalse(x instanceof IWorker); } } @Test /** * see comments in traitsExample.drl */ public void testTraitsInRules() { KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); kbuilder.add(new ClassPathResource("org/drools/semantics/trait/java/model/traitsExample.drl"), ResourceType.DRL); System.out.println(kbuilder.getErrors()); Assert.assertEquals(0, kbuilder.getErrors().size()); KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); kbase.addKnowledgePackages(kbuilder.getKnowledgePackages()); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); List list = new ArrayList(); List toks = new ArrayList(); ksession.setGlobal("list",list); ksession.setGlobal("tokens",toks); ksession.insert("trigger"); ksession.fireAllRules(); assertEquals(4,list.size()); assertTrue(list.contains("Math")); assertTrue(list.contains("Zchool")); assertTrue(list.contains(IStudent_Impl.BREATHE)); assertTrue(list.contains(214)); // one for the human // one for the thing wrapping the human // three updates assertEquals(5, toks.size()); } }