001/*
002 * Copyright (c) 2004-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2015-08-24 22:43:10 +0000 (Mon, 24 Aug 2015) $' 
007 * '$Revision: 33629 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.ecoinformatics.seek.sms;
031
032import java.io.File;
033import java.io.FileOutputStream;
034import java.io.FileWriter;
035import java.io.OutputStream;
036import java.util.Collections;
037import java.util.Iterator;
038import java.util.Vector;
039
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.kepler.util.DotKeplerManager;
043import org.kepler.util.StaticResources;
044
045import com.hp.hpl.jena.ontology.Individual;
046import com.hp.hpl.jena.ontology.OntClass;
047import com.hp.hpl.jena.ontology.OntModel;
048import com.hp.hpl.jena.ontology.OntModelSpec;
049import com.hp.hpl.jena.ontology.tidy.Checker;
050import com.hp.hpl.jena.rdf.model.ModelFactory;
051import com.hp.hpl.jena.rdf.model.Property;
052import com.hp.hpl.jena.rdql.Query;
053import com.hp.hpl.jena.rdql.QueryEngine;
054import com.hp.hpl.jena.rdql.QueryResults;
055import com.hp.hpl.jena.rdql.ResultBinding;
056import com.hp.hpl.jena.vocabulary.OWL;
057import com.hp.hpl.jena.vocabulary.RDF;
058
059import ptolemy.kernel.ComponentEntity;
060import ptolemy.kernel.CompositeEntity;
061import ptolemy.kernel.util.Attribute;
062import ptolemy.kernel.util.ChangeRequest;
063import ptolemy.kernel.util.NamedObj;
064import ptolemy.kernel.util.Workspace;
065import ptolemy.moml.EntityLibrary;
066import ptolemy.moml.MoMLChangeRequest;
067import ptolemy.vergil.tree.EntityTreeModel;
068import ptolemy.vergil.tree.VisibleTreeModel;
069
070/**
071 * AnnotationEngine Interface { AnnotationEngine instance() Vector
072 * getDefaultConceptNames() void addActorAnnotation(String lsid, String
073 * conceptName) EntityTreeModel buildDefaultActorLibrary() Vector search(String
074 * classname) Vector search(String classname, boolean approx) }
075 * 
076 *@author berkley
077 *@created February 17, 2005
078 */
079public class AnnotationEngine {
080        private static final Log log = LogFactory.getLog(AnnotationEngine.class
081                        .getName());
082        private static final boolean isDebugging = log.isDebugEnabled();
083
084        private boolean _debug = false;
085        // set to false to suppress debug messages
086        private static AnnotationEngine _engine = null;
087        // singleton instance
088        private KeplerLocalLSIDService _libService = KeplerLocalLSIDService
089                        .instance();
090
091        // should be a listener interface
092        private EntityTreeModel _currentTreeModel = null;
093
094        // paths to necessary files
095        // TODO: make these more robust ...
096        private String KEPLER = System.getProperty("KEPLER");
097        //TODO FIXME hardcoded path:
098        private String LOCAL_PATH = KEPLER + "/common/configs/" + StaticResources.RESOURCEBUNDLE_DIR + "/";
099        private String ONTO_FILE = LOCAL_PATH + "ontology.owl";
100        private String SCHEMA_FILE = LOCAL_PATH + "annotation-schema.owl";
101        // private String ANNOTATION_FILE = LOCAL_PATH + "annotations.owl";
102        private static final String ANNOTATION_FILE = new File(DotKeplerManager.getInstance()
103                        .getTransientModuleDirectory("sms"),"annotations").toString();
104
105        // constants to hold namespaces
106        private String SCHEMA_NS = "http://seek.ecoinformatics.org/annotation-schema#";
107        private String ONTO_NS = "http://seek.ecoinformatics.org/ontology#";
108        // private String ONTO_NS = "urn:lsid:localhost:ontology:1:1#";
109
110        // the annotation model
111        private OntModel _ontModelAnnotations;
112        // the annotation model
113        private OntModel _ontModelAnnotationSchema;
114
115        /**
116         * Constructor
117         */
118        protected AnnotationEngine() {
119                initialize();
120        }
121
122        /**
123         *@return The unique instance of this class This must be called to
124         *         create/obtain an instance of the engine.
125         */
126        public static AnnotationEngine instance() {
127                if (_engine == null) {
128                        _engine = new AnnotationEngine();
129                }
130                return _engine;
131        }
132
133        /**
134         * For testing... dumps out the annotations using n3 syntax.
135         */
136        public void print() {
137                // N3 also works
138                _ontModelAnnotations.write(System.out, "N-TRIPLE");
139        }
140
141        /**
142         * Reload the ontology and annotation information. Eventually, should check
143         * if changes were made prior to loading.
144         */
145        protected void initialize() {
146                try {
147
148                        Checker validate = new Checker(false);
149                        debug("loading ontologies and annotations ... ", false);
150                        // create and read the annotation schema and annotations
151                        _ontModelAnnotationSchema = ModelFactory.createOntologyModel(
152                                        OntModelSpec.OWL_MEM_RDFS_INF, null);
153                        _ontModelAnnotationSchema.read("file:" + SCHEMA_FILE);
154                        // _ontModelAnnotations.read("file:" + ANNOTATION_FILE);
155                        _ontModelAnnotations = ModelFactory.createOntologyModel(
156                                        OntModelSpec.OWL_MEM_RDFS_INF, null);
157                        // _ontModelAnnotations.read("File:" + ANNOTATION_FILE);
158                        debug("OK", true);
159                } catch (Exception e) {
160                        e.printStackTrace();
161                }
162        }
163
164        /**
165         * Not Implemented.
166         * 
167         *@return The defaultConceptNames value
168         */
169        public Vector<String> getDefaultConceptNames() {
170                Iterator iter = ontModelOntology().listClasses();
171                Vector<String> result = new Vector<String>();
172                while (iter.hasNext()) {
173                        OntClass c = (OntClass) iter.next();
174                        result.add(c.getLocalName());
175                }
176                Collections.sort(result);
177                return result;
178        }
179
180        /**
181         * Annotate an actor (via the lsid) with a given ontology concept id
182         * 
183         *@param lsid
184         *            The feature to be added to the ActorAnnotation attribute
185         *@param conceptName
186         *            The feature to be added to the ActorAnnotation attribute
187         *@exception Exception
188         *                Description of the Exception
189         */
190        public void addActorAnnotation(String lsid, String conceptName)
191                        throws Exception {
192                // TODO: this should change with new schema !!!
193
194                // check if the lsid/actor exists
195                if (!_libService.isAssignedLSID(lsid)) {
196                        throw new Exception("Id not registered: " + lsid);
197                }
198
199                // get the class with the conceptName
200                OntClass conceptClass = ontModelOntology().getOntClass(
201                                ONTO_NS + conceptName);
202                if (conceptClass == null) {
203                        throw new Exception("Not a valid ontology concept: " + conceptName);
204                }
205
206                // make sure we don't already have the same annotation; if so,
207                // clean-return
208                if (annotationExists(lsid, conceptClass)) {
209                        return;
210                }
211
212                // construct an anonymous Actor instance, and assign an lsid property to
213                // it
214                OntClass actorClass = _ontModelAnnotationSchema.getOntClass(SCHEMA_NS
215                                + "Actor");
216                Individual actorInst = _ontModelAnnotations
217                                .createIndividual(actorClass);
218                Property lsidProp = _ontModelAnnotationSchema.getOntProperty(SCHEMA_NS
219                                + "lsid");
220                actorInst.addProperty(lsidProp, lsid);
221
222                // construct an anonymous ItemTag instance, and assign taggedItem ->
223                // Actor and the concept
224                OntClass itemTagClass = _ontModelAnnotationSchema.getOntClass(SCHEMA_NS
225                                + "ItemTag");
226                Individual itemTagInst = _ontModelAnnotations
227                                .createIndividual(conceptClass);
228                itemTagInst.addRDFType(conceptClass);
229                Property taggedItemProp = _ontModelAnnotationSchema
230                                .getOntProperty(SCHEMA_NS + "taggedItem");
231                itemTagInst.addProperty(taggedItemProp, actorInst);
232
233                // construct the Annotation and assign annotates -> ItemTag
234                OntClass annotClass = _ontModelAnnotationSchema.getOntClass(SCHEMA_NS
235                                + "Annotation");
236                Individual annotInst = _ontModelAnnotations
237                                .createIndividual(annotClass);
238                Property annotatesProp = _ontModelAnnotationSchema
239                                .getProperty(SCHEMA_NS + "annotates");
240                annotInst.addProperty(annotatesProp, itemTagInst);
241
242                // output addition
243                debug("Saving Annotation...", false);
244                File file = new File(ANNOTATION_FILE);
245                // first, write the xml header:
246                FileWriter writer = new FileWriter(file);
247                writer.write("<?xml version=\"1.0\"?>\n");
248                writer.close();
249                // now open the stream for append
250                OutputStream output = new FileOutputStream(file, true);
251                _ontModelAnnotations.write(output, "RDF/XML-ABBREV");
252                output.close();
253                // need to update the library
254                debug("OK", true);
255
256                // rebuild the library
257                NamedObj obj = _libService.getData(lsid);
258                rebuildActorLibrary(obj, conceptClass.getLabel(null));
259        }
260
261        /**
262         * helper function
263         * 
264         *@param obj
265         *            Description of the Parameter
266         *@param conceptLabel
267         *            Description of the Parameter
268         */
269        private void rebuildActorLibrary(NamedObj obj, String conceptLabel) {
270                // iterate through the tree to the node with the name of the
271                // conceptClass
272
273                if (conceptLabel == null) {
274                        return;
275                }
276
277                addToCurrentTreeModel((NamedObj) _currentTreeModel.getRoot(), obj,
278                                conceptLabel);
279        }
280
281        /**
282         * recursive helper function
283         * 
284         *@param parent
285         *            The feature to be added to the ToCurrentTreeModel attribute
286         *@param obj
287         *            The feature to be added to the ToCurrentTreeModel attribute
288         *@param conceptLabel
289         *            The feature to be added to the ToCurrentTreeModel attribute
290         */
291        private void addToCurrentTreeModel(NamedObj parent, NamedObj obj,
292                        String conceptLabel) {
293                // return if parent is null or not a composite entity
294                if (parent == null || !(parent instanceof CompositeEntity)) {
295                        return;
296                }
297                CompositeEntity folder = (CompositeEntity) parent;
298                // see if parent matches concept label and add, otherwise iterate
299                if (folder.getName().equals(conceptLabel)) {
300                        // add and do issue change request
301                        try {
302                                NamedObj obj2 = (NamedObj) obj.clone(folder.workspace());
303                                if (obj2 instanceof ComponentEntity) {
304                                        ((ComponentEntity) obj2).setContainer(folder);
305                                } else if (obj2 instanceof Attribute) {
306                                        ((Attribute) obj2).setContainer(folder);
307                                } else {
308                                        return;
309                                }
310                                ChangeRequest request = new MoMLChangeRequest(obj2,
311                                                "adding object to actor library");
312                                obj2.requestChange(request);
313                                obj2.executeChangeRequests();
314                        } catch (Exception e) {
315                                e.printStackTrace();
316                        }
317                } else {
318                        // get children and iterate
319                        Iterator iter = folder.entityList().iterator();
320                        while (iter.hasNext()) {
321                                addToCurrentTreeModel((NamedObj) iter.next(), obj, conceptLabel);
322                        }
323                }
324        }
325
326        /**
327         * helper function. returns true if an annotation of actor with lsid and of
328         * type conceptname exists
329         * 
330         *@param lsid
331         *            Description of the Parameter
332         *@param conceptClass
333         *            Description of the Parameter
334         *@return Description of the Return Value
335         */
336        private boolean annotationExists(String lsid, OntClass conceptClass) {
337                try {
338                        Vector resultIds = new Vector();
339                        String str = "";
340                        str += "select ?item \n";
341                        str += "where  (?res, " + "<" + RDF.type + ">, " + "<"
342                                        + conceptClass.getURI() + ">), \n";
343                        str += "       (?res, " + "<" + SCHEMA_NS
344                                        + "taggedItem>, ?item), \n";
345                        str += "       (?item, " + "<" + SCHEMA_NS + "lsid>, '" + lsid
346                                        + "')";
347                        Query q = new Query(str);
348                        q.setSource(_ontModelAnnotations);
349                        QueryResults results = new QueryEngine(q).exec();
350                        return results.hasNext();
351                } catch (Exception e) {
352                        e.printStackTrace();
353                        return false;
354                }
355        }
356
357        /**
358         * Constructs the default actor library that is loaded, e.g., when Kepler is
359         * started in graph mode.
360         * 
361         *@param _libPane
362         *            Description of the Parameter
363         *@return Description of the Return Value
364         */
365        public EntityTreeModel buildDefaultActorLibrary() {
366                EntityTreeModel libraryModel = _libService.getBasicActorLibrary();
367                try {
368                        debug("building tree model ... ", false);
369                        EntityLibrary root = new EntityLibrary();
370                        OntClass thing = ontModelOntology().getOntClass(OWL.Thing.getURI());
371                        Iterator iter = thing.listSubClasses(true);
372                        // true means direct subclasses
373                        while (iter.hasNext()) {
374                                buildTreeModel(root, (OntClass) iter.next());
375                        }
376                        debug("OK", true);
377                        _currentTreeModel = new VisibleTreeModel(root);
378                        return _currentTreeModel;
379                } catch (Exception e) {
380                        e.printStackTrace();
381                }
382
383                return null;
384        }
385
386        /**
387         * recursive helper function to build the new tree model
388         * 
389         *@param parent
390         *            Description of the Parameter
391         *@param currClass
392         *            Description of the Parameter
393         */
394        private void buildTreeModel(EntityLibrary parent, OntClass currClass) {
395                try {
396                        EntityLibrary folder = new EntityLibrary();
397                        Workspace workspace = folder.workspace();
398
399                        // check to make sure it isn't some non-ontology class TODO:
400                        // change this to hold a list of target namespaces obtained
401                        // from loading the ontology
402                        if (!currClass.getNameSpace().equals(ONTO_NS)) {
403                                return;
404                        }
405
406                        folder.setName(currClass.getLabel(null));
407                        folder.setContainer(parent);
408
409                        // get all annotated item instances of the class
410                        Iterator iter = getMatchingAnnotatedItemIds(currClass).iterator();
411
412                        // for each id add the item with the same id (nested loop; ugh!)
413                        while (iter.hasNext()) {
414                                Object id = iter.next();
415                                NamedObj obj = _libService.getData(id.toString());
416                                if (obj != null) {
417                                        // clone into current workspace
418                                        NamedObj obj2 = (NamedObj) obj.clone(workspace);
419                                        if (obj2 instanceof ComponentEntity) {
420                                                ((ComponentEntity) obj2).setContainer(folder);
421                                        } else if (obj2 instanceof Attribute) {
422                                                ((Attribute) obj2).setContainer(folder);
423                                        }
424                                }
425                        }
426                        // get direct subclasses
427                        iter = currClass.listSubClasses(true);
428                        while (iter.hasNext()) {
429                                OntClass c = (OntClass) iter.next();
430                                buildTreeModel(folder, c);
431                        }
432
433                } catch (Exception e) {
434                        e.printStackTrace();
435                }
436        }
437
438        /**
439         * helper function. returns a list of id strings that are instances of the
440         * currClass uses a rdql query.
441         * 
442         *@param currClass
443         *            Description of the Parameter
444         *@return The matchingAnnotatedItemIds value
445         */
446        private Vector<Object> getMatchingAnnotatedItemIds(OntClass currClass) {
447                try {
448                        Vector<Object> resultIds = new Vector<Object>();
449                        String str = "";
450                        str += "select ?lsid \n";
451                        str += "where  (?res, " + "<" + RDF.type + ">, " + "<"
452                                        + currClass.getURI() + ">), \n";
453                        str += "       (?res, " + "<" + SCHEMA_NS
454                                        + "taggedItem>, ?item), \n";
455                        str += "       (?item, " + "<" + SCHEMA_NS + "lsid>, ?lsid)";
456                        Query q = new Query(str);
457                        q.setSource(_ontModelAnnotations);
458                        QueryResults results = new QueryEngine(q).exec();
459                        while (results.hasNext()) {
460                                ResultBinding res = (ResultBinding) results.next();
461                                // Return from get is null if not found
462                                Object obj = res.get("lsid");
463                                // obj will be a Jena object: resource, property or RDFNode.
464                                if (obj != null) {
465                                        resultIds.add(obj);
466                                }
467                        }
468                        results.close();
469                        return resultIds;
470                } catch (Exception e) {
471                        e.printStackTrace();
472                        return new Vector<Object>();
473                }
474        }
475
476        /**
477         * for testing
478         * 
479         *@param str
480         *            Description of the Parameter
481         *@param newline
482         *            Description of the Parameter
483         */
484        private void debug(String str, boolean newline) {
485                if (!_debug) {
486                        return;
487                }
488                if (newline) {
489                        System.out.println(str);
490                } else {
491                        System.out.print("ANNOTATION ENGINE: " + str);
492                }
493        }
494
495        /**
496         * search default case
497         * 
498         *@param classname
499         *            Description of the Parameter
500         *@return Description of the Return Value
501         */
502        public Vector<NamedObj> search(String classname) {
503                return search(classname, true);
504        }
505
506        /**
507         *@param classname
508         *            the classname (as a term/keyword) to search for
509         *@param approx
510         *            if true, find approximate term matches, and if false, do exact
511         *            matches Searches the ontology matching the term and for
512         *            matches, finds annotated items. For example,
513         *            search("RichnessIndex") finds all actors that instantiate
514         *            either the class named "RichnessIndex" or one of its
515         *            subclasses.
516         *@return Description of the Return Value
517         */
518        public Vector<NamedObj> search(String classname, boolean approx) {
519                if (isDebugging) log.debug("search("+classname+", "+approx+")");
520                // check if valid concept name
521                if (!isValidClass(classname, approx)) {
522                        debug("didn't find classname", true);
523                        return new Vector<NamedObj>();
524                }
525
526                Vector<NamedObj> result = new Vector<NamedObj>();
527                // find all the matching class names and their subclasses
528                Vector<OntClass> classes = getMatchingClassNames(classname, approx);
529                Iterator<OntClass> iter = classes.iterator();
530                while (iter.hasNext()) {
531                        // find the matching lsids for the class name
532                        OntClass cls = iter.next();
533                        Vector<Object> ids = getMatchingAnnotatedItemIds(cls);
534                        Iterator<Object> idIter = ids.iterator();
535                        while (idIter.hasNext()) {
536                                // get the associated objects
537                                NamedObj obj = _libService.getData(idIter.next().toString());
538                                if (obj != null && !result.contains(obj)) {
539                                        result.add(obj);
540                                }
541                                // add the results
542                        }
543                }
544                return result;
545        }
546
547        /**
548         * helper function to find all class names
549         * 
550         *@param classname
551         *            Description of the Parameter
552         *@param approx
553         *            Description of the Parameter
554         *@return The matchingClassNames value
555         */
556        private Vector<OntClass> getMatchingClassNames(String classname, boolean approx) {
557                Vector<OntClass> initResult = new Vector<OntClass>();
558                // get all classes in ontology
559                Iterator iter = ontModelOntology().listClasses();
560                while (iter.hasNext()) {
561                        // find classes that have a similar name
562                        OntClass cls = (OntClass) iter.next();
563                        if (approx && approxMatch(cls.getLocalName(), classname)) {
564                                initResult.add(cls);
565                        } else if (!approx && classname.equals(cls.getLocalName())) {
566                                initResult.add(cls);
567                        }
568                }
569                Vector<OntClass> result = (Vector) initResult.clone();
570                iter = initResult.iterator();
571                while (iter.hasNext()) {
572                        // find all subclasses of direct classes
573                        OntClass cls = (OntClass) iter.next();
574                        Iterator clsIter = cls.listSubClasses(false);
575                        // direct = false
576                        while (clsIter.hasNext()) {
577                                OntClass subCls = (OntClass) clsIter.next();
578                                if (!result.contains(subCls)) {
579                                        result.add(subCls);
580                                }
581                        }
582                }
583                return result;
584        }
585
586        /**
587         * helper function for search. Note that we are assuming uniformity in that
588         * there is only a single namespace for the ontology, and so below, we only
589         * check the fragments of the URIs of the classes in the ontology.
590         * 
591         * This operation is just a simple optimization pre-step.
592         * 
593         *@param classname
594         *            Description of the Parameter
595         *@param approx
596         *            Description of the Parameter
597         *@return The validClass value
598         */
599        private boolean isValidClass(String classname, boolean approx) {
600                // get the classes in the onto
601                Iterator iter = ontModelOntology().listClasses();
602                while (iter.hasNext()) {
603                        OntClass c = (OntClass) iter.next();
604                        if (!approx && c.getLocalName().equals(classname)) {
605                                return true;
606                        }
607                        if (approx && approxMatch(c.getLocalName(), classname)) {
608                                return true;
609                        }
610                }
611                return false;
612        }
613
614        /**
615         * helper function for search
616         * 
617         *@param val1
618         *            Description of the Parameter
619         *@param val2
620         *            Description of the Parameter
621         *@return Description of the Return Value
622         */
623        private boolean approxMatch(String val1, String val2) {
624                val1 = val1.toLowerCase();
625                val2 = val2.toLowerCase();
626                if (val1.indexOf(val2) != -1 || val2.indexOf(val1) != -1) {
627                        return true;
628                }
629                return false;
630        }
631
632        private OntModel ontModelOntology() {
633                OntologyCatalog catalog = OntologyCatalog.instance();
634                return catalog.getDefaultOntology();
635        }
636
637        /**
638         * The main program for the AnnotationEngine class for testing: prints out
639         * the annotations.
640         * 
641         *@param args
642         *            The command line arguments
643         */
644        public static void main(String[] args) {
645                AnnotationEngine eng = AnnotationEngine.instance();
646                eng.print();
647        }
648
649}