001package org.biopax.paxtools.converter;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import org.biopax.paxtools.controller.*;
006import org.biopax.paxtools.model.BioPAXElement;
007import org.biopax.paxtools.model.BioPAXFactory;
008import org.biopax.paxtools.model.BioPAXLevel;
009import org.biopax.paxtools.model.Model;
010import org.biopax.paxtools.model.level2.*;
011import org.biopax.paxtools.model.level3.*;
012import org.biopax.paxtools.util.Filter;
013
014import java.io.IOException;
015import java.lang.reflect.InvocationTargetException;
016import java.lang.reflect.Method;
017import java.net.URLEncoder;
018import java.util.*;
019
020/**
021 * Upgrades BioPAX L1 and L2 to Level 3.
022 * 
023 * Notes:
024 * - it does not fix existing BioPAX errors
025 * - most but not all things are converted (e.g., complex.ORGANISM property cannot...)
026 * - phy. entities "clones" - because, during L1 or L2 data read, all re-used pEPs are duplicated... (TODO filter after the conversion)
027 * 
028 * @author rodch
029 *
030 */
031public final class LevelUpgrader extends AbstractTraverser implements ModelFilter {
032        private static final Log log = LogFactory.getLog(LevelUpgrader.class);
033        private static final String l3PackageName = "org.biopax.paxtools.model.level3.";
034
035        private BioPAXFactory factory;
036        private Properties classesmap;
037        private Properties propsmap;
038
039        /**
040         * For mapping level2 enums to level2.
041         */
042        private Map<Object, Object> enumMap;
043
044        /**
045         * Several PEPs in L2 will correspond to a PE in L3. Id of PE will be the ID of one of the
046         * related PEPs. This map will point Ids of PEPs to the ID of PE.
047         */
048        private Map<String, String> pep2PE;
049
050        
051
052        /**
053         * Default Constructor 
054         * that also loads 'classesmap' and 'propsmap' 
055         * from the properties files.
056         */
057        public LevelUpgrader() {
058                super(SimpleEditorMap.L2, new Filter<PropertyEditor>() {
059                        public boolean filter(PropertyEditor editor) {
060                                return !editor.getProperty().equals("STOICHIOMETRIC-COEFFICIENT"); 
061                                // will be set manually (pEPs special case)
062                        }
063                  },
064                  new Filter<PropertyEditor>() {
065                        public boolean filter(PropertyEditor editor) {
066                                return 
067                                        !( 
068                                                editor.getProperty().equals("ORGANISM")
069                                                && complex.class.isAssignableFrom(editor.getDomain())
070                                        ); 
071                                // L3 Complex has no 'organism' property
072                        }
073                  }
074                );
075                
076                factory = BioPAXLevel.L3.getDefaultFactory();
077                // load L2-L3 classes map
078                classesmap = new Properties();
079                try {
080                        classesmap.load(getClass().getResourceAsStream("classesmap.properties"));
081                // load L2-L3 properties map
082                propsmap = new Properties();
083                propsmap.load(getClass().getResourceAsStream("propsmap.properties"));
084                } catch (IOException e) {
085                        throw new RuntimeException(e);
086                }
087
088                enumMap = new HashMap<Object, Object>();
089        }
090        
091        /**
092         * Constructor
093         * @param factory BioPAX objects factory implementation
094         */
095        public LevelUpgrader(BioPAXFactory factory) {
096                this();
097                this.factory = factory;
098        }
099
100        /**
101         * Converts a BioPAX Model, Level 1 or 2, to the Level 3.
102         *
103         * @param model BioPAX model to upgrade
104         * @return new Level3 model
105         */
106        public Model filter(Model model) {
107                if(model == null || model.getLevel() != BioPAXLevel.L2) 
108                {
109                        if(model != null && log.isInfoEnabled()) 
110                                log.info("Model is " + model.getLevel());
111                        return model; // nothing to do
112                }
113
114                preparePep2PEIDMap(model);
115
116                final Model newModel = factory.createModel();
117                newModel.getNameSpacePrefixMap().putAll(model.getNameSpacePrefixMap());
118                newModel.setXmlBase(model.getXmlBase());
119                
120                // facilitate the conversion
121                normalize(model);
122                
123                // First, map classes (and only set ID) if possible (pre-processing)
124                for(BioPAXElement bpe : model.getObjects()) {
125                        Level3Element l3element = mapClass(bpe);
126                        if(l3element != null) {
127                                newModel.add(l3element);
128                        } else {
129                                if(log.isDebugEnabled())
130                                        log.debug("Skipping " + bpe 
131                                                + " " + bpe.getModelInterface().getSimpleName());
132                        }
133                }
134                
135                /* process each L2 element (mapping properties), 
136                 * except for pEPs and oCVs that must be processed as values
137                 * (anyway, we do not want dangling elements)
138                 */
139                for(BioPAXElement e : model.getObjects()) {
140                        if(e instanceof physicalEntityParticipant
141                                || e instanceof openControlledVocabulary) {
142                                continue;
143                        }
144                        
145                        // map properties
146                        traverse((Level2Element) e, newModel);
147                }
148
149                if(log.isInfoEnabled())
150                        log.info("Done. The new model contains " 
151                                        + newModel.getObjects().size()
152                                        + " BioPAX individuals.");
153                
154                // fix new model (e.g., add PathwayStep's processes to the pathway components ;))
155                normalize(newModel);
156                
157                return newModel;
158        }
159
160
161        /*
162         * Fixes several problems in the BioPAX model
163         */
164        private void normalize(Model model) {
165                /* If a pE participates via pEP, no problem.
166                 * Otherwise, - there is a special case. 
167                 * 
168                 * physicalEntity PARTICIPANT takes part in interactions
169                 * either directly or via physicalEntityParticipant (pEP). 
170                 * With pEP, the corresponding L3 PE and ER have been 
171                 * already created (above). However, although (if no pEPs used) 
172                 * we got Complex and basic PhysicalEntity objects created in L3,
173                 * and others (protein, dna, etc.) became ER, we still have to 
174                 * create another PEs for all of them and set their ER property (where exists).
175                 */
176                for (interaction itr : model.getObjects(interaction.class)) {
177                        if(itr instanceof conversion || itr instanceof control) 
178                                continue; // cannot have pE participants anyway (only pEPs)
179                        for (InteractionParticipant ip : itr.getPARTICIPANTS()) {
180                                if (ip instanceof physicalEntity) { 
181                                        physicalEntity pe = (physicalEntity) ip;
182                                        // create a new pEP
183                                        String newId = itr.getRDFId() + "_"     + getLocalId(pe);
184                                        physicalEntityParticipant pep = model.addNew(physicalEntityParticipant.class, newId);
185                                        pep.setPHYSICAL_ENTITY(pe);
186                                        // no other properties though
187                                        // reset participant
188                                        itr.removePARTICIPANTS(pe);
189                                        itr.addPARTICIPANTS(pep);
190                                }
191                        }
192                }
193                
194                // ...adding step processes to the pathway components (L3) is NOT always required
195                
196                // TODO remove cloned UtilityClass elements
197                
198                // set 'name' for simple physical entities (because pEPs didn't have names)
199                for(EntityReference er : model.getObjects(EntityReference.class)) {
200                        for(SimplePhysicalEntity spe : er.getEntityReferenceOf()) {
201                                // after the conversion, it's always empty.., but let's double-check
202                                if(spe.getName().isEmpty()) {
203                                        spe.getName().addAll(er.getName());
204                                }
205                                
206                                if(spe.getDisplayName() == null || spe.getDisplayName().trim().length() == 0) {
207                                        spe.setDisplayName(er.getDisplayName());
208                                }
209                        }
210                }
211                
212        }
213
214        
215        // creates L3 classes and sets IDs
216        private Level3Element mapClass(BioPAXElement bpe) {
217                Level3Element newElement = null;
218                
219                if(bpe instanceof physicalEntityParticipant)
220                {
221                        String id = pep2PE.get(bpe.getRDFId());
222
223                        if(id == null) {
224                                log.warn("No mapping possible for " + bpe.getRDFId());
225                                return null;
226                        }
227                        else if (id.equals(bpe.getRDFId()))
228                        {
229                                // create a new simplePhysicalEntity
230                                //(excluding Complex and basic PhysicalEntity that map directly and have no ERs)
231                                newElement = createSimplePhysicalEntity((physicalEntityParticipant)bpe);
232                        }
233                }
234                else if(!(bpe instanceof openControlledVocabulary)) // skip oCVs
235                {
236                        // using classesmap.properties to map other types
237                        String type = bpe.getModelInterface().getSimpleName();  
238                        String newType = classesmap.getProperty(type).trim();
239                        if (newType != null && factory.canInstantiate(factory.getLevel().getInterfaceForName(newType)))
240            {
241                                newElement = (Level3Element) factory.create(newType, bpe.getRDFId());
242                        } else {
243                                if(log.isDebugEnabled())
244                                        log.debug("No mapping found for " + type);
245                                return null;
246                        }
247                }
248
249                return newElement;
250        }
251
252
253        /*
254         * Create L3 simple PE type using the L2 pEP.
255         *
256         * When pEP's PHYSICAL_ENTITY is either complex
257         * or basic physicalEntity, null will be the result.
258         */
259        private SimplePhysicalEntity createSimplePhysicalEntity(physicalEntityParticipant pep) {
260                physicalEntity pe2 = pep.getPHYSICAL_ENTITY();
261                return createSimplePhysicalEntity(pe2, pep.getRDFId());
262        }
263
264        private SimplePhysicalEntity createSimplePhysicalEntity(physicalEntity pe2, String id) {
265                SimplePhysicalEntity e = null;
266                if(pe2 instanceof protein) {
267                        e = factory.create(Protein.class, id);
268                } else if(pe2 instanceof dna) {
269                        e = factory.create(DnaRegion.class, id);
270                } else if (pe2 instanceof rna) {
271                        e = factory.create(RnaRegion.class, id);
272                } else if (pe2 instanceof smallMolecule) {
273                        e = factory.create(SmallMolecule.class, id);
274                }
275                return e;
276        }
277
278
279        /*
280         * Creates a specific ControlledVocabulary subclass
281         * and adds to the new model
282         */
283        private ControlledVocabulary convertAndAddVocabulary(openControlledVocabulary value,
284                        Level2Element parent, Model newModel, PropertyEditor newEditor)
285        {
286                String id = ((BioPAXElement) value).getRDFId();
287
288                if (!newModel.containsID(id)) {
289                        if (newEditor != null) {
290                                newModel.addNew(newEditor.getRange(), id);
291                                // copy properties
292                                traverse(value, newModel);
293                        } else {
294                                log.warn("Cannot Convert CV: " + value
295                                        + " (for prop.: " + newEditor + ")");
296                        }
297                } 
298                
299                return (ControlledVocabulary) newModel.getByID(id);
300        }
301
302        // parent class's abstract method implementation
303        protected void visit(Object value, BioPAXElement parent,
304                        Model newModel, PropertyEditor editor)
305        {
306                        if(editor != null && editor.isUnknown(value)) {
307                                return;
308                        }
309
310                        String parentType = parent.getModelInterface().getSimpleName();
311                        BioPAXElement newParent = null;
312                        Object newValue = value;
313                        String newProp = propsmap.getProperty(editor.getProperty());
314
315                        // special case (PATHWAY-COMPONENTS maps to pathwayComponent or pathwayOrder)
316                        if(parent instanceof pathway && value instanceof pathwayStep
317                                        && editor.getProperty().equals("PATHWAY-COMPONENTS")) {
318                                newProp = "pathwayOrder";
319                        }
320
321                        // for pEPs, getting the corresponding simple PE or Complex is different
322                        if(parent instanceof physicalEntityParticipant) {
323                                newParent = getMappedPE((physicalEntityParticipant) parent, newModel);
324                        } else {
325                                newParent = newModel.getByID(parent.getRDFId());
326                        }
327
328                        // bug check!
329                        if(newParent == null) {
330                                throw new IllegalAccessError("Of " + value +
331                                        ", parent " + parentType + " : " + parent +
332                                        " is not yet in the new model: ");
333                        }
334
335                        PropertyEditor newEditor =
336                                SimpleEditorMap.L3.getEditorForProperty(newProp, newParent.getModelInterface());
337
338                        if(value instanceof Level2Element)
339                        // not a String, Enum, or primitive type
340                        {
341                                // when pEP, create/add stoichiometry!
342                                if(value instanceof physicalEntityParticipant)
343                                {
344                                        physicalEntityParticipant pep = (physicalEntityParticipant) value;
345                                        newValue = getMappedPE(pep, newModel);
346
347                                        float coeff = (float) pep.getSTOICHIOMETRIC_COEFFICIENT();
348                                        if (coeff > 1 ) { //!= BioPAXElement.UNKNOWN_DOUBLE) {
349                                          if(parent instanceof conversion || parent instanceof complex) {
350                                                PhysicalEntity pe3 = (PhysicalEntity) newValue;
351                                                Stoichiometry stoichiometry = factory
352                                                        .create(Stoichiometry.class,
353                                    pe3.getRDFId() + "-stoichiometry" + Math.random());
354                                                stoichiometry.setStoichiometricCoefficient(coeff);
355                                                stoichiometry.setPhysicalEntity(pe3);
356                                                //System.out.println("parent=" + parent + "; phy.ent.=" + pep + "; coeff=" + coeff);
357                                                if (parent instanceof conversion) {
358                                                        // (pep) value participates in the conversion interaction
359                                                        Conversion conv = (Conversion) newModel
360                                                                .getByID(parent.getRDFId());
361                                                        conv.addParticipantStoichiometry(stoichiometry);        
362                                                } else {
363                                                        // this (pep) value is component of the complex
364                                                        Complex cplx = (Complex) newModel.getByID(parent.getRDFId());
365                                                        cplx.addComponentStoichiometry(stoichiometry);
366                                                } 
367                                                
368                                                newModel.add(stoichiometry);
369                                                
370                                          } else {
371                                                if (log.isDebugEnabled())
372                                                        log.debug(pep + " STOICHIOMETRIC_COEFFICIENT is "
373                                                        + coeff + ", but the pEP's parent is not " +
374                                                        "a conversion or complex - " + parent);
375                                          }
376                                        }
377                                        
378                                        traverse(pep, newModel);
379                                }
380                                else if(value instanceof openControlledVocabulary) 
381                                {
382                                        // create the proper type ControlledVocabulary instance 
383                                        newValue = convertAndAddVocabulary((openControlledVocabulary)value, 
384                                                (Level2Element)parent, newModel, newEditor);
385                                }
386                                else
387                                {
388                                        String id = ((Level2Element) value).getRDFId();
389                                        newValue = newModel.getByID(id);
390                                }
391                        }
392                        else if (value.getClass().isEnum())
393                        {
394                                newValue = getMatchingEnum(value);
395                        }
396                        else
397                        {
398                                // relationshipXref.RELATIONSHIP-TYPE range changed (String -> RelationshipTypeVocabulaty)
399                                if(parent instanceof relationshipXref && editor.getProperty().equals("RELATIONSHIP-TYPE")) {
400                                        String id = URLEncoder.encode(value.toString());
401                                        if(!newModel.containsID(id)) {
402                                                RelationshipTypeVocabulary cv = (RelationshipTypeVocabulary) 
403                                                        newModel.addNew(newEditor.getRange(), id);
404                                                cv.addTerm(value.toString().toLowerCase());
405                                                newValue = cv;
406                                        } else {
407                                                newValue = newModel.getByID(id);
408                                        }
409                                }
410                        }
411                        
412                        if(newValue == null) {
413                                log.warn("Skipping:  " + parent + "." + editor.getProperty() 
414                                        + "=" + value + " ==> " + newParent.getRDFId() 
415                                        + "." + newProp + "=NULL");
416                                return;
417                        }
418                        
419                        if (newProp != null) {
420                                if (newEditor != null){
421                                        setNewProperty(newParent, newValue, newEditor);
422                                } else // Special mapping for 'AVAILABILITY' and 'DATA-SOURCE'!
423                                  if(parent instanceof physicalEntity) {
424                                        // find parent pEP(s)
425                                        Set<physicalEntityParticipant> ppeps = ((physicalEntity)parent).isPHYSICAL_ENTITYof();
426                                        // if several pEPs use the same phy.entity, we get this property/value cloned...
427                                        for(physicalEntityParticipant pep: ppeps) {
428                                                //find proper L3 physical entity
429                                                newParent = getMappedPE(pep, newModel);
430                                                if(newParent != null) {
431                                                        newEditor = 
432                                                                SimpleEditorMap.L3.getEditorForProperty(
433                                                                                newProp, newParent.getModelInterface());
434                                                        setNewProperty(newParent, newValue, newEditor);
435                                                } else { // bug!
436                                                        log.error("Cannot find converted PE to map the property " 
437                                                                + editor.getProperty() 
438                                                                + " of physicalEntity " 
439                                                                + parent + " (" + parentType + ")");
440                                                }
441                                        }
442                                } else {
443                                        log.info("Skipping property " 
444                                                + editor.getProperty() 
445                                                + " in " + parentType + " to " +
446                                                newParent.getModelInterface().getSimpleName() 
447                                                + " convertion (" + parent + ")");
448                                }
449                        } else {
450                                log.warn("No mapping defined for property: " 
451                                                + parentType + "."
452                                                + editor.getProperty());
453                        } 
454                }
455
456
457        private void setNewProperty(BioPAXElement newParent, Object newValue, PropertyEditor newEditor) {
458                if(newEditor != null){
459                        if(newEditor instanceof PrimitivePropertyEditor) {
460                                newValue = newValue.toString();
461                        }
462                        newEditor.setValueToBean(newValue, newParent);
463                }               
464        }
465
466        /*
467         * pEP->PE; pE->ER class mapping was done for "simple" entities;
468         * for complex and the "basic" pE, it is pE->PE mapping
469         * (although pEPs were skipped, their properties are to save anyway)
470         */
471        private PhysicalEntity getMappedPE(physicalEntityParticipant pep, Model newModel)
472        {
473                String id = pep2PE.get(pep.getRDFId());
474                physicalEntity pe2er = pep.getPHYSICAL_ENTITY();
475                
476                if(id == null || pe2er == null)
477                        throw new IllegalAccessError("Illegal pEP (cannot convert): " 
478                                + pep.getRDFId()); 
479                
480                BioPAXElement pe = newModel.getByID(id);
481                String inf = "pEP " + pep + " that contains " 
482                        + pe2er.getModelInterface().getSimpleName();
483                if(!isSimplePhysicalEntity(pe2er)) {
484                        if(pe == null) {
485                                if(log.isDebugEnabled())
486                                        log.debug(inf + " gets new ID: " + pe2er.getRDFId());
487                                pe = newModel.getByID(pe2er.getRDFId());
488                        } else { // error: complex and basic p.entity's pEP 
489                                throw new IllegalAccessError("Illegal conversion of pEP: " + inf); 
490                        }
491                } else if(pe == null){
492                        // for a pEP having a simple PE, a new PE should be created
493                        throw new IllegalAccessError("No PE for: " + inf
494                                        + " found in the new Model");
495                }
496                
497                return (PhysicalEntity) pe;
498        }
499
500        private boolean isSimplePhysicalEntity(Level2Element pe2) {
501                return (pe2 instanceof protein
502                                || pe2 instanceof dna
503                                || pe2 instanceof rna
504                                || pe2 instanceof smallMolecule);
505        }
506        
507    /**
508     * Gets the local part of the BioPAX element ID.
509     * 
510     * @param bpe BioPAX object
511     * @return id - local part of the URI
512     */
513        static public String getLocalId(BioPAXElement bpe) {
514                String id = bpe.getRDFId();
515                return (id != null) ? id.replaceFirst("^.+[#/]", "") : null; //greedy pattern matches everything up to the last '/' or '#'
516        }
517
518        protected Object getMatchingEnum(Object o)
519        {
520                assert o.getClass().isEnum();
521
522                if (enumMap.containsKey(o))
523                        return enumMap.get(o);
524
525                if (!propsmap.containsKey(o.toString()))
526                {
527                        enumMap.put(o, null);
528                        return null;
529                }
530
531                String l2Name = o.getClass().getName();
532                l2Name = l2Name.substring(l2Name.lastIndexOf(".") + 1);
533
534                if (!classesmap.containsKey(l2Name))
535                {
536                        log.error("There is no class mapping for enum " + o.getClass() + " in classesmap");
537                        return null;
538                }
539
540                String l3Name = classesmap.getProperty(l2Name);
541
542                assert l3Name != null;
543
544                String l3value = propsmap.getProperty(o.toString());
545
546                assert l3value != null;
547
548                Class<?> cls = null;
549                try
550                {
551                        cls = Class.forName(l3PackageName + l3Name);
552                }
553                catch (ClassNotFoundException e)
554                {
555                        log.error("Cannot find class " + l3PackageName + l3Name);
556                        //e.printStackTrace();
557                }
558
559                assert cls != null;
560
561                Method meth = null;
562                try
563                {
564                        meth = cls.getMethod("valueOf", String.class);
565                }
566                catch (NoSuchMethodException e)
567                {
568                        log.error("No valueOf method here. Is this possible ?!");
569                        e.printStackTrace();
570                }
571
572                assert meth != null;
573
574                Object l3enum = null;
575                try
576                {
577                        l3enum = meth.invoke(null, l3value);
578                }
579                catch (IllegalAccessException e)
580                {
581                        log.error("Cannot invoke method " 
582                                + meth + " - " + e);
583                        //e.printStackTrace();
584                }
585                catch (InvocationTargetException e)
586                {
587                        log.error("Cannot invoke method " + meth 
588                                + " - " + e);
589                        //e.printStackTrace();
590                }
591
592                enumMap.put(o, l3enum);
593                return l3enum;
594        }
595
596        private List<Set<physicalEntityParticipant>> getPepsGrouped(physicalEntity pe)
597        {
598                List<Set<physicalEntityParticipant>> list = new ArrayList<Set<physicalEntityParticipant>>();
599
600                for (physicalEntityParticipant pep : pe.isPHYSICAL_ENTITYof())
601                {
602                        boolean added = false;
603
604                        for (Set<physicalEntityParticipant> group : list)
605                        {
606                                physicalEntityParticipant first = group.iterator().next();
607
608                                if (first.isInEquivalentState(pep))
609                                {
610                                        group.add(pep);
611                                        added = true;
612                                        break;
613                                }
614                        }
615
616                        if (!added)
617                        {
618                                Set<physicalEntityParticipant> group = new HashSet<physicalEntityParticipant>();
619                                group.add(pep);
620                                list.add(group);
621                        }
622                }
623                return list;
624        }
625
626        private Map<String, String> getPep2StateIDMapping(physicalEntity pe)
627        {
628                List<Set<physicalEntityParticipant>> sets = getPepsGrouped(pe);
629
630                Map<String, String> map = new HashMap<String, String>();
631
632                for (Set<physicalEntityParticipant> set : sets)
633                {
634                        physicalEntityParticipant first = set.iterator().next();
635
636                        for (physicalEntityParticipant pep : set)
637                        {
638                                map.put(pep.getRDFId(), first.getRDFId());
639                        }
640                }
641                return map;
642        }
643
644        protected void preparePep2PEIDMap(Model model)
645        {
646                assert model.getLevel() == BioPAXLevel.L2;
647
648                pep2PE = new HashMap<String, String>();
649
650                for (physicalEntity pe : model.getObjects(physicalEntity.class))
651                {
652                        Map<String, String> map = getPep2StateIDMapping(pe);
653
654                        pep2PE.putAll(map);
655                }
656        }
657}