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}