001package org.biopax.paxtools.controller;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import org.biopax.paxtools.model.BioPAXElement;
006import org.biopax.paxtools.model.Model;
007
008import java.util.*;
009
010/**
011 * Utility class to merge multiple biopax models into one. Note that this merger does not preserve
012 * the integrity of the passed models. Target will be a merged model and source will become
013 * unusable.
014 */
015
016public class Merger implements Visitor
017{
018// ------------------------------ FIELDS ------------------------------
019
020        private static final Log log = LogFactory.getLog(Merger.class);
021
022        private final Traverser traverser;
023
024        private final HashMap<Integer, List<BioPAXElement>> equivalenceMap =
025                        new HashMap<Integer, List<BioPAXElement>>();
026        private final EditorMap map;
027
028        // Keep track of merged elements
029        private final HashSet<BioPAXElement> mergedElements =
030                        new HashSet<BioPAXElement>();
031
032        // Keep track of new elements
033        private final HashSet<BioPAXElement> addedElements = new HashSet<BioPAXElement>();
034
035        // --------------------------- CONSTRUCTORS ---------------------------
036
037        /**
038         * @param map a class to editor map containing the editors for the elements of models to be
039         *            modified.
040         */
041        public Merger(EditorMap map)
042        {
043                this.map = map;
044                traverser = new Traverser(map, this);
045        }
046
047// ------------------------ INTERFACE METHODS ------------------------
048
049        // --------------------- Interface Visitor ---------------------
050
051        /**
052         * Checks whether <em>model</em> contains <em>bpe</em> element, and if it does, then it updates the
053         * value of the equivalent element for <em>bpe</em> by using the specific <em>editor</em>.
054         *
055         * @param domain owner 
056         * @param range property value
057         * @param model  model containing the equivalent element's equivalent
058         * @param editor biopax property editor specific for the value type to be updated
059         */
060        public void visit(BioPAXElement domain, Object range, Model model, PropertyEditor editor)
061        {
062                if (range != null && range instanceof BioPAXElement)
063                {
064                        BioPAXElement bpe = (BioPAXElement) range;
065
066                        // do nothing if you already inserted this
067
068                        if (!model.contains(bpe))
069                        {
070                                //if there is an identical
071                                if (model.getByID(bpe.getRDFId()) != null)
072                                {
073                                        if (editor.isMultipleCardinality())
074                                        {
075                                                editor.removeValueFromBean(bpe, domain);
076                                        }
077                                        editor.setValueToBean(getIdentical(bpe), domain);
078                                }
079                        }
080                }
081        }
082
083// -------------------------- OTHER METHODS --------------------------
084
085        /**
086         * After a merge is accomplished, this set will contain the merged elements. This is not an
087         * essential method for paxtools functionality, but it may be useful for 3rd party applications.
088         *
089         * @return a hashet of merged elements in the target
090         * @see #merge
091         */
092        public HashSet<BioPAXElement> getMergedElements()
093        {
094                return this.mergedElements;
095        }
096
097        /**
098         * After a merge is accomplished, this set will contain the newly added elements. This is not an
099         * essential method for paxtools functionallity, but it may be useful for 3rd party applications.
100         *
101         * @return a hashet of newly added elements in the target
102         * @see #merge
103         */
104        public HashSet<BioPAXElement> getAddedElements()
105        {
106                return this.addedElements;
107        }
108
109        /**
110         * Merges the <em>source</em> models into <em>target</em> model.
111         *
112         * @param target  model into which merging process will be done
113         * @param sources model(s) that are going to be merged with <em>target</em>
114         */
115        public void merge
116                        (Model target, Model... sources)
117        {
118                // Empty merged and added elements sets
119                mergedElements.clear();
120                addedElements.clear();
121
122                // Fill equivalence map with objects from  target model
123                Set<BioPAXElement> targetElements = target.getObjects();
124                for (BioPAXElement t_bpe : targetElements)
125                {
126                        this.addIntoEquivalanceMap(t_bpe);
127                }
128
129                // Try to insert every biopax element in every source one by one
130                for (Model source : sources)
131                {
132                        Set<BioPAXElement> sourceElements = source.getObjects();
133                        for (BioPAXElement bpe : sourceElements)
134                        {
135                                insert(target, bpe);
136                        }
137                }
138        }
139
140        /**
141         * Inserts a BioPAX element into the <em>target</em> model if it does not contain an equivalent;
142         * but if does, than it updates the equivalent using this element's values.
143         *
144         * @param target model into which bpe will be inserted
145         * @param bpe    BioPAX element to be inserted into target
146         */
147        private void insert(Model target, BioPAXElement bpe)
148        {
149                // do nothing if you already inserted this
150                if (!target.contains(bpe))
151                {
152                        //if there is an identical (in fact, "equal") object
153                        BioPAXElement ibpe = target.getByID(bpe.getRDFId());
154                        if (ibpe != null && ibpe.equals(bpe))
155                        /* - ibpe.equals(bpe) can be 'false' here, because, 
156                         * even though the above !target.contains(bpe) is true,
157                         * it probably compared objects using '==' operator
158                         * (see the ModelImpl for details)
159                         */
160                        {
161                                updateObjectFields(bpe, ibpe, target);
162                                // We have a merged element, add it into the tracker
163                                mergedElements.add(ibpe);
164                        }
165                        else
166                        {
167                                target.add(bpe);
168                                this.addIntoEquivalanceMap(bpe);
169                                traverser.traverse(bpe, target);
170                                // We have a new element, add it into the tracker
171                                addedElements.add(bpe);
172                        }
173                }
174        }
175
176        /**
177         * Searches the target model for an identical of given BioPAX element, and returns this element if
178         * it finds it.
179         *
180         * @param bpe BioPAX element for which equivalent will be searched in target.
181         * @return the BioPAX element that is found in target model, if there is none it returns null
182         */
183        private BioPAXElement getIdentical(BioPAXElement bpe)
184        {
185                int key = bpe.hashCode();
186                List<BioPAXElement> list = equivalenceMap.get(key);
187                if (list != null)
188                {
189                        for (BioPAXElement other : list)
190                        {
191                                if (other.equals(bpe))
192                                {
193                                        return other;
194                                }
195                        }
196                }
197                return null;
198        }
199
200        /**
201         * Updates each value of <em>existing</em> element, using the value(s) of <em>update</em>.
202         *
203         * @param update   BioPAX element of which values are used for update
204         * @param existing BioPAX element to be updated
205         * @param target
206         */
207        private void updateObjectFields(BioPAXElement update,
208                                        BioPAXElement existing, Model target)
209        {
210                Set<PropertyEditor> editors =
211                                map.getEditorsOf(update);
212                for (PropertyEditor editor : editors)
213                {
214                        updateObjectFieldsForEditor(editor, update, existing, target);
215                }
216
217//              if (!update.getRDFId().equals(existing.getRDFId()))
218//              {
219//TODO addNew a unification xref
220//                      if(existing instanceof XReferrable)
221//                      {
222//                              ((XReferrable) existing).addXref(fa);
223//                      }
224//              }
225        }
226
227        /**
228         * Updates the value of <em>existing</em> element, using the value of <em>update</em>. Editor is
229         * the used for the modification.
230         *
231         * @param editor   editor for the specific value to be updated
232         * @param update   BioPAX element of which value is used for the update
233         * @param existing BioPAX element to be updated
234         * @param target
235         */
236        private void updateObjectFieldsForEditor(PropertyEditor editor,
237                                                 BioPAXElement update,
238                                                 BioPAXElement existing,
239                                                 Model target)
240        {
241                if (editor.isMultipleCardinality())
242                {
243                        for (Object updateValue : editor.getValueFromBean(update))
244                        {
245                                updateField(editor, updateValue, existing, target);
246                        }
247                }
248                else
249                {
250                        Object existingValue = editor.getValueFromBean(existing);
251                        Object updateValue = editor.getValueFromBean(update);
252
253                        if (editor.isUnknown(existingValue))
254                        {
255                                if (!editor.isUnknown(updateValue))
256                                {
257                                        updateField(editor, updateValue, existing, target);
258                                }
259                        }
260                        else
261                        {
262                                if (!existingValue.equals(updateValue))
263                                {
264                                        log.warn("Mismatch in single cardinality field:" +
265                                                 existingValue + ":" + updateValue);
266                                        log.warn("Using existing value");
267                                }
268                        }
269                }
270        }
271
272        /**
273         * Updates <em>existing</em>, using the <em>updateValue</em> by the editor.
274         *
275         * @param editor      editor for the specific value to be updated
276         * @param updateValue the value for the update
277         * @param existing    BioPAX element to be updated
278         * @param target
279         */
280        private void updateField(PropertyEditor editor, Object updateValue,
281                                 BioPAXElement existing, Model target)
282        {
283                if (updateValue instanceof BioPAXElement)
284                {
285                        BioPAXElement bpe = (BioPAXElement) updateValue;
286                        //Now there are two possibilities
287
288
289                        BioPAXElement ibpe = target.getByID(bpe.getRDFId());
290                        //1. has an identical in the target
291                        if (ibpe != null)
292                        {
293                                updateValue = ibpe;
294                        }
295                        //2. it has no identical in the target
296                        //I do not have to do anything as it will eventually
297                        //be moved.
298
299                }
300                editor.setValueToBean(updateValue, existing);
301        }
302
303        /**
304         * Adds a BioPAX element into the equivalence map.
305         *
306         * @param bpe BioPAX element to be added into the map.
307         */
308        private void addIntoEquivalanceMap(BioPAXElement bpe)
309        {
310                int key = bpe.hashCode();
311                List<BioPAXElement> list = equivalenceMap.get(key);
312                if (list == null)
313                {
314                        list = new ArrayList<BioPAXElement>();
315                        equivalenceMap.put(key, list);
316                }
317                list.add(bpe);
318        }
319}