001package org.biopax.paxtools.io;
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.deltaGprimeO;
011import org.biopax.paxtools.model.level2.kPrime;
012import org.biopax.paxtools.model.level2.physicalEntityParticipant;
013import org.biopax.paxtools.util.BioPaxIOException;
014import org.biopax.paxtools.util.IllegalBioPAXArgumentException;
015
016import java.io.*;
017import java.util.Map;
018
019
020/**
021 *
022 */
023public abstract class BioPAXIOHandlerAdapter implements BioPAXIOHandler
024{
025        private boolean treatNilAsNull;
026
027        private boolean convertingFromLevel1ToLevel2 = false;
028
029        private boolean fixReusedPEPs = false;
030
031        private static final Log log = LogFactory.getLog(BioPAXIOHandlerAdapter.class);
032
033        protected BioPAXLevel level;
034
035        protected BioPAXFactory factory;
036
037        protected EditorMap editorMap;
038
039        protected Map<String, String> namespaces;
040
041        protected static final String rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
042
043        protected static final String rdfs = "http://www.w3.org/2000/01/rdf-schema#";
044
045        protected String bp; //current BioPAX namespace prefix value
046
047        protected static final String xsd = "http://www.w3.org/2001/XMLSchema#";
048
049        protected static final String owl = "owl=http://www.w3.org/2002/07/owl#";
050
051        protected String base;
052
053        public BioPAXIOHandlerAdapter()
054        {
055                this(null, null);
056        }
057
058        public BioPAXIOHandlerAdapter(BioPAXLevel level)
059        {
060                this(null, level);
061        }
062
063        public BioPAXIOHandlerAdapter(BioPAXFactory factory, BioPAXLevel level)
064        {
065                resetLevel(level, factory);
066        }
067
068        /**
069         * Updates the level and factory for this I/O
070         * (final - because used in the constructor)
071         * @param level BioPAX Level
072         * @param factory concrete BioPAX factory impl.
073         */
074        protected final void resetLevel(BioPAXLevel level, BioPAXFactory factory)
075        {
076                this.level = (level != null) ? level : BioPAXLevel.L3;
077                this.factory = (factory != null) ? factory : this.level.getDefaultFactory();
078
079                // default flags
080                if (this.level == BioPAXLevel.L1)
081                {
082                        this.convertingFromLevel1ToLevel2 = true;
083                        this.fixReusedPEPs = true;
084                } else if (this.level == BioPAXLevel.L2)
085                {
086                        this.fixReusedPEPs = true;
087                }
088
089                bp = this.level.getNameSpace();
090
091                resetEditorMap(); //implemented by concrete subclasses
092        }
093
094        /**
095         * Updates the member EditorMap for the new BioPAX level and factory (different implementations of
096         * EditorMap can be used in modules, e.g. SimpleEditorMap and JenaEditorMap.)
097         */
098        protected abstract void resetEditorMap();
099
100        /**
101         * According the BioPAX documentation, it is illegal to reuse a Physical Entity Participant (PEP).
102         * If this value is set to <em>true</em> (default value), a reused PEP will be duplicated while
103         * converting the OWL file into a model.
104         * @see org.biopax.paxtools.controller.ReusedPEPHelper
105         */
106
107        private ReusedPEPHelper reusedPEPHelper;
108
109        /**
110         * Enables (true) or disables (false) the fixing of reused peps.
111         * @param fixReusedPEPs true if fixing is desired
112         * @see #fixReusedPEPs
113         */
114        public void fixReusedPEPs(boolean fixReusedPEPs)
115        {
116                this.fixReusedPEPs = fixReusedPEPs;
117        }
118
119        /**
120         * A workaround for a common issue that was present in BioCyc exports. For non-present values BioCyc exports
121         * contained the string "NIL".
122         * @param treatNILasNull true/false
123         * @deprecated This problem is fixed and this option is no longer needed for recent BioCyc exports. Enable only if
124         *             you are parsing a legacy export.
125         */
126        public void treatNilAsNull(boolean treatNILasNull)
127        {
128                this.treatNilAsNull = treatNILasNull;
129        }
130
131        /**
132         * This method enables silent conversion of Level1 to Level2. If disabled the method will throw an error.
133         * @param convertingFromLevel1ToLevel2 true/false
134         * @deprecated BioPAX Level 1 exports are extremely rare and obsolete.
135         */
136        public void setConvertingFromLevel1ToLevel2(boolean convertingFromLevel1ToLevel2)
137        {
138                this.convertingFromLevel1ToLevel2 = convertingFromLevel1ToLevel2;
139        }
140
141        /**
142         * @return true if this reader is treating string 'NIL' as Null
143         * @deprecated This problem is fixed and this option is no longer needed for recent BioCyc exports. Enable only if
144         *             you are parsing a legacy export.
145         */
146        public boolean isTreatNilAsNull()
147        {
148                return treatNilAsNull;
149        }
150
151        /**
152         * @return true is converting Level1 to Level2 silently.
153         */
154        public boolean isConvertingFromLevel1ToLevel2()
155        {
156                return convertingFromLevel1ToLevel2;
157        }
158
159        /**
160         * Workaround for a very common Level 2 issue. Most level2 exports in the past reused PhysicalEntityParticipants
161         * as if they represent states. This is problematic because PEPs also contain stoichiometry information which is
162         * specific to a reaction. BioPAX spec says that PEPs should not be reused across reactions.
163         *
164         * As Level2 exports getting obsolete this method is slated for deprecation.
165         * @return true if this Handler automatically splits reused PEPs to interaction specific PEPS.
166         */
167        public boolean isFixReusedPEPs()
168        {
169                return fixReusedPEPs;
170        }
171
172        /**
173         * This is a helper class initialized only if fixReusedPEPs is true.
174         * @return helper object
175         */
176        protected ReusedPEPHelper getReusedPEPHelper()
177        {
178                return reusedPEPHelper;
179        }
180
181
182        public BioPAXFactory getFactory()
183        {
184                return factory;
185        }
186
187
188        public void setFactory(BioPAXFactory factory)
189        {
190                this.factory = factory;
191        }
192
193
194        public EditorMap getEditorMap()
195        {
196                return editorMap;
197        }
198
199        public void setEditorMap(EditorMap editorMap)
200        {
201                this.editorMap = editorMap;
202        }
203
204        public BioPAXLevel getLevel()
205        {
206                return level;
207        }
208
209        /**
210         * This method reads multiple files and returns a merged model.
211         * @deprecated  experimental and incomplete; e.g., files are to be ordered, i.e.,
212         *                              a former file should not point to the latter. Use it at your own risk.
213         *                              Or, use #convertFromOWL to get independent models; then, try merging them...
214         * @param files Dependency ordered biopax owl file names
215         * @return a merged model.
216         * @throws java.io.FileNotFoundException when any file can not be found
217         */
218        @Deprecated
219        public Model convertFromMultipleOwlFiles(String... files) throws FileNotFoundException
220        {
221                Model model = this.factory.createModel();
222
223                for (String file : files)
224                {
225                        FileInputStream in = new FileInputStream(new File(file));
226                        if (log.isDebugEnabled())
227                        {
228                                log.debug("start reading file:" + file);
229                        }
230//                      createAndBind(in, model); //TODO why this line is currently commented out; since when?..
231
232                        if (log.isDebugEnabled())
233                        {
234                                log.debug("read file: " + file);
235                        }
236                }
237                return model;
238        }
239
240
241        /**
242         * Reads a BioPAX model from an OWL file input stream (<em>in</em>) and converts it to a model.
243         * @param in inputStream from which the model will be read
244         * @return an empty model in case of invalid input.
245         */
246        public Model convertFromOWL(InputStream in)
247        {
248                init(in);
249
250                //cache the namespaces.
251                namespaces = this.readNameSpaces();
252
253                autodetectBiopaxLevel(); // this may update level, editorMap and factory!
254
255//              bp = level.getNameSpace();
256
257                Model model = factory.createModel();
258
259                model.getNameSpacePrefixMap().putAll(namespaces);
260
261                model.setXmlBase(base);
262
263                boolean fixingPEPS = model.getLevel() == BioPAXLevel.L2 && this.isFixReusedPEPs();
264                if (fixingPEPS)
265                {
266                        reusedPEPHelper = new ReusedPEPHelper(model);
267                }
268
269                createAndBind(model);
270
271                if (fixingPEPS)
272                {
273                        this.getReusedPEPHelper().copyPEPFields();
274                }
275                
276                reset(in);
277
278                return model;
279        }
280
281        protected void reset(InputStream in)
282        {
283                try
284                {
285                        in.close();
286
287                }
288                catch (IOException e)
289                {
290                        throw new RuntimeException("Failed to close the file");
291                }
292
293        }
294
295        private void autodetectBiopaxLevel()
296        {
297                BioPAXLevel filelevel = null;
298                for (String namespaceValue : namespaces.values())
299                {
300                        filelevel = BioPAXLevel.getLevelFromNameSpace(namespaceValue);
301                        if (filelevel != null)
302                        {
303                                if (log.isDebugEnabled())
304                                        log.debug("Auto-detected biopax " + filelevel + " (current settings are for Level " + level + ")");
305                                break;
306                        }
307                }
308
309                if (filelevel == null)
310                {
311                        log.error("Cannot detect biopax level.");
312                        throw new BioPaxIOException("Cannot detect biopax level.");
313                } else if (level != filelevel)
314                {
315                        if (log.isDebugEnabled()) log.debug("Reset to the default factory for the detected BioPAX level.");
316                        resetLevel(filelevel, filelevel.getDefaultFactory());
317                }
318        }
319
320        /**
321         * This method is called by the reader for each OWL instance in the OWL model. It creates a POJO instance, with
322         * the given id and inserts it into the model. The inserted object is "clean" in the sense that its properties are
323         * not set yet.
324         *
325         * Implementers of this abstract class can override this method to inject code during object creation.
326         * @param model to be inserted
327         * @param id of the new object. The model should not contain another object with the same ID.
328         * @param localName of the class to be instantiated.
329         */
330        protected void createAndAdd(Model model, String id, String localName)
331        {
332                BioPAXElement bpe = this.getFactory().create(localName, id);
333
334                if (log.isTraceEnabled())
335                {
336                        log.trace("id:" + id + " " + localName + " : " + bpe);
337                }
338                /* null might occur here,
339                 * so the following is to prevent the NullPointerException
340                 * and to continue the model assembling.
341                 */
342                if (bpe != null)
343                {
344                        model.add(bpe);
345                } else
346                {
347                        log.warn("null object created during reading. It might not be an official BioPAX class.ID: " + id +
348                                 " Class " +
349                                 "name " + localName);
350                }
351        }
352
353        /**
354         * This method provides a hook for the implementers of this abstract class to perform the initial reading from the
355         * input stream.
356         *
357         * @param in BioPAX RDF/XML input stream
358         */
359        protected abstract void init(InputStream in);
360
361        /**
362         * This method provides a hook for the implementers of this abstract class to set the namespaces of the model.
363         * @return a map of namespaces.
364         */
365        protected abstract Map<String, String> readNameSpaces();
366
367        /**
368         * This method provides a hook for the implementers of this abstract class to create objects themselves and bind
369         * the properties to the objects.
370         * @param model to be populated
371         */
372        protected abstract void createAndBind(Model model);
373
374        /**
375         * Several workarounds for two properties, DeltaG and KEQ, that changed from Level1 for Level2
376         * @param editor that is responsible for the property that is currently being assigned to the bpe. This method
377         * will only react if the editor is one of Delta-G or KEQ. These two properties have integer values in L1 but were
378         * upgraded to 5-tuples called DeltaGPrime0 and KPrime in L2.
379         * @param bpe that is being bound.
380         * @param model that is being populated.
381         * @param value of the property which is an integer.
382         * @return a modified BioPAX L2 element, DeltaGPrime0 or Kprime if the editor is Delta-G or KEQ respectively.
383         *         Null otherwise.
384         * @deprecated BioPAX Level 1 exports are extremely rare and obsolete.
385         */
386        protected BioPAXElement L1ToL2Fixes(PropertyEditor editor, BioPAXElement bpe, Model model, String value)
387        {
388                BioPAXElement created = null;
389
390                if (this.isConvertingFromLevel1ToLevel2())
391                {
392                        if (editor.getProperty().equals("DELTA-G"))
393                        {
394                                deltaGprimeO aDeltaGprime0 = model.addNew(deltaGprimeO.class, (bpe.getRDFId() + "-DELTA-G"));
395                                aDeltaGprime0.setDELTA_G_PRIME_O(Float.valueOf(value));
396                                created = aDeltaGprime0;
397                        }
398                        if (editor.getProperty().equals("KEQ"))
399                        {
400                                kPrime aKPrime = model.addNew(kPrime.class, (bpe.getRDFId() + "-KEQ"));
401                                aKPrime.setK_PRIME(Float.valueOf(value));
402                                created = aKPrime;
403                        }
404
405                }
406
407                return created;
408        }
409
410        /**
411         * This method currently only fixes reusedPEPs if the option is set. As L2 is becoming obsolete this method will be
412         * slated for deprecation.
413         * @param bpe to be bound
414         * @param value to be assigned.
415         * @return a "fixed" value.
416         */
417        protected Object resourceFixes(BioPAXElement bpe, Object value)
418        {
419                if (this.isFixReusedPEPs() && value instanceof physicalEntityParticipant)
420                {
421                        value = this.getReusedPEPHelper().fixReusedPEP((physicalEntityParticipant) value, bpe);
422                }
423                return value;
424        }
425
426        /**
427         * This method binds the value to the bpe. Actual assignment is handled by the editor - but this method performs
428         * most of the workarounds and also error handling due to invalid parameters.
429         * @param valueString to be assigned
430         * @param editor that maps to the property
431         * @param bpe to be bound
432         * @param model to be populated.
433         */
434        protected void bindValue(String valueString, PropertyEditor editor, BioPAXElement bpe, Model model)
435        {
436
437                if (log.isDebugEnabled())
438                {
439                        log.debug("Binding: " + bpe + '(' + bpe.getModelInterface() + " has  " + editor + ' ' + valueString);
440                }
441                Object value = valueString;
442
443                if (editor instanceof ObjectPropertyEditor)
444                {
445                        value = model.getByID(valueString);
446                        value = resourceFixes(bpe, value);
447                        if (value == null)
448                        {
449                                value = L1ToL2Fixes(editor, bpe, model, valueString);
450                                if (value == null)
451                                {
452                                        throw new IllegalBioPAXArgumentException(
453                                                        "Illegal or Dangling Value/Reference: " + valueString + " (element: " + bpe.getRDFId() +
454                                                        " property: " + editor.getProperty() + ")");
455                                }
456                        } else if (this.isTreatNilAsNull() && valueString.trim().equalsIgnoreCase("NIL"))
457                        {
458                                value = null;
459                        }
460                }
461                if (editor == null)
462                {
463                        log.error("Editor is null. This probably means an invalid BioPAX property. Failed to set " + valueString);
464                } else
465                {
466                        editor.setValueToBean(value, bpe);
467                }
468        }
469
470        /**
471         * Paxtools maps BioPAX:comment (L3) and BioPAX:COMMENT (L2) to rdf:comment. This method handles that.
472         * @param bpe to be bound.
473         * @return a property editor responsible for editing comments.
474         */
475        protected StringPropertyEditor getRDFCommentEditor(BioPAXElement bpe)
476        {
477                StringPropertyEditor editor;
478                Class<? extends BioPAXElement> inter = bpe.getModelInterface();
479                if (this.getLevel().equals(BioPAXLevel.L3))
480                {
481                        editor = (StringPropertyEditor) this.getEditorMap().getEditorForProperty("comment", inter);
482                } else
483                {
484                        editor = (StringPropertyEditor) this.getEditorMap().getEditorForProperty("COMMENT", inter);
485                }
486                return editor;
487        }
488
489        /**
490         * Similar to {@link BioPAXIOHandler#convertToOWL(org.biopax.paxtools.model.Model,
491         * java.io.OutputStream)} (org.biopax.paxtools.model.Model, Object)}, but
492         * extracts a sub-model, converts it into BioPAX (OWL) format,
493         * and writes it into the outputStream.
494         * Saved data can be then read via {@link BioPAXIOHandler}
495         * interface (e.g., {@link SimpleIOHandler}).
496         * @param model model to be converted into OWL format
497         * @param outputStream output stream into which the output will be written
498         * @param ids optional list of "root" element absolute URIs (all direct/indirect child objects are auto-exported as well)
499         */
500        public void convertToOWL(Model model, OutputStream outputStream, String... ids)
501        {
502                if (ids.length == 0)
503                {
504                        convertToOWL(model, outputStream);
505                } else
506                {
507                        Model m = model.getLevel().getDefaultFactory().createModel();
508                        m.setXmlBase(model.getXmlBase());
509
510                        //to avoid 'nextStep' that may lead to infinite loops -
511                        Fetcher fetcher = new Fetcher(SimpleEditorMap.get(model.getLevel()), 
512                                        Fetcher.nextStepFilter);
513
514                        for (String uri : ids)
515                        {
516                                BioPAXElement bpe = model.getByID(uri);
517                                if (bpe != null)
518                                {
519                                        fetcher.fetch(bpe, m);
520                                }
521                        }
522
523                        convertToOWL(m, outputStream);
524                }
525        }
526}