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;
007import org.biopax.paxtools.util.Filter;
008
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashSet;
012import java.util.Set;
013
014/**
015 * A "simple" BioPAX merger, a utility class to merge
016 * 'source' BioPAX models or a set of elements into the target model,
017 * using (URI) identity only. Merging into a normalized,
018 * self-integral model normally makes sense and gives better results
019 * (but it depends on the application though).
020 * 
021 * One can also "merge" a model to itself, i.e.: merge(target,target),
022 * or to an empty one, which adds all implicit child elements 
023 * to the model and makes it self-integral.
024 * 
025 * Note, "URI identity" means that it does not copy
026 * a source element to the target model if the target already has an element 
027 * with the same URI. However, it will update (re-wire) all the object 
028 * properties of new elements to make sure they do not refer to any objects 
029 * outside the updated target model.
030 * 
031 * We do not guarantee the integrity of the source models after the merge is done
032 * (some object properties will refer to target elements).
033 * 
034 * Finally, although called Simple Merger, it is in fact an advanced BioPAX utility,
035 * which should be used wisely. Otherwise, it can actually waste resources.
036 * So, consider using model.add(..), model.addNew(..) approach first (or instead),
037 * especially, when you're adding "new" things (ID not present in the target model),
038 * or/and target model does not contain any references to the source or another one, etc.
039 */
040public class SimpleMerger
041{
042        private static final Log LOG = LogFactory.getLog(SimpleMerger.class);
043
044        private final EditorMap map;
045        
046        private Filter<BioPAXElement> mergeObjPropOf;
047
048        /**
049         * @param map a class to editor map for the elements to be modified.
050         */
051        public SimpleMerger(EditorMap map)
052        {
053                this.map = map;
054        }
055
056        /** 
057         * @param map a class to editor map for the elements to be modified.
058         * @param mergeObjPropOf when not null, all multiple-cardinality properties 
059         *                                              of a source biopax object that passes this filter are updated
060         *                                              and also copied to the corresponding (same URI) target object,
061         *                      unless the source and target are the same thing 
062         *                      (in which case, we simply migrate object properties 
063         *                      to target model objects). 
064         */
065        public SimpleMerger(EditorMap map, Filter<BioPAXElement> mergeObjPropOf)
066        {
067                this(map);
068                this.mergeObjPropOf = mergeObjPropOf;
069        }
070
071        /**
072         * Merges the <em>source</em> models into <em>target</em> model,
073         * one after another (in the order they are listed).
074         * 
075         * If the target model is self-integral (complete) or empty, then
076         * the result of the merge will be also a complete model (contain 
077         * unique objects with unique URIs, all objects referenced from 
078         * any other object in the model).
079         * 
080         * Source models do not necessarily have to be complete and may even
081         * indirectly contain different objects of the same type with the same 
082         * URI. Though, in most cases, one probably wants target model be complete
083         * or empty for the best possible results. So, if your target is incomplete, 
084         * or you are not quite sure, then do simply merge it as the first source 
085         * to a new empty model or itself (or call {@link Model#repair()} first).
086         *       
087         * @param target model into which merging process will be done
088         * @param sources models to be merged/updated to <em>target</em>; order can be important
089         */
090        public void merge(Model target, Model... sources)
091        {
092                for (Model source : sources)
093                        if (source != null)
094                                merge(target, source.getObjects());
095        }
096
097
098        /**
099         * Merges the <em>elements</em> and all their child biopax objects
100         * into the <em>target</em> model.
101         * 
102         * @see #merge(Model, Model...) for details about the target model.
103         * 
104         * @param target model into which merging will be done
105         * @param elements elements that are going to be merged/updated to <em>target</em>
106         */
107        public void merge(Model target, Collection<? extends BioPAXElement> elements)
108        {
109                @SuppressWarnings("unchecked")
110                final Fetcher fetcher = new Fetcher(map);
111                
112                // Auto-complete source 'elements' by discovering all the implicit elements there
113                // copy all elements, as the collection can be immutable or unsafe to add elements to
114                final Set<BioPAXElement> sources = new HashSet<BioPAXElement>(elements);
115                for(BioPAXElement se : elements) {
116                        sources.addAll(fetcher.fetch(se));
117                }
118                                
119                // Next, we only copy elements having new URIs -
120                for (BioPAXElement bpe : sources)
121                {
122                        /* if there exists target element with the same id, 
123                         * do not copy this one! (this 'source' element will 
124                         * be soon replaced with the target's, same id, one 
125                         * in all parent objects)
126                         */
127                        if (!target.containsID(bpe.getRDFId()))
128                        {
129                                /*
130                                 * Warning: other than the default (ModelImpl) target Model 
131                                 * implementations may add child elements recursively (e.g., 
132                                 * using jpa cascades/recursion); it might also override target's
133                                 * properties with the corresponding ones from the source, even
134                                 * though SimpleMerger is not supposed to do this; also, is such cases,
135                                 * the number of times this loop body is called can be less that
136                                 * the number of elements in sourceElements set that weren't
137                                 * originally present in the target model, or - even equals to
138                                 * one)
139                                 */
140                                target.add(bpe);
141                        } 
142                }
143
144                // Finally, update object references
145                for (BioPAXElement bpe : sources) {
146                        updateObjectFields(bpe, target);
147                }
148                
149        }
150
151
152        /**
153         * Merges the <em>source</em> element (and its "downstream" dependents)
154         * into <em>target</em> model.
155         * 
156         * @see #merge(Model, Collection)
157         * 
158         * @param target the BioPAX model to merge into
159         * @param source object to add or merge
160         */
161        public void merge(Model target, BioPAXElement source)
162        {
163                merge(target, Collections.singleton(source));
164        }
165
166
167        /**
168         * Updates each value of <em>existing</em> element, using the value(s) of <em>update</em>.
169         * @param source BioPAX element of which values are used for update
170         * @param target the BioPAX model
171         */
172        private void updateObjectFields(BioPAXElement source, Model target)
173        {
174                //Skip if target model had a different object with the same URI as the source's,
175                //and no Filter was set (mergeObjPropOf is null); i.e., when
176                //we simply want the source to be replaced with an object
177                //having the same type, URI that was already in the target model.
178                BioPAXElement keep = target.getByID(source.getRDFId());
179                if(keep != source && mergeObjPropOf==null) 
180                {
181                        return; //nothing to do
182                }
183                
184                Set<PropertyEditor> editors = map.getEditorsOf(source);
185                for (PropertyEditor editor : editors)
186                {
187                        if (editor instanceof ObjectPropertyEditor)
188                        {
189                                //copy prop. values (to avoid concurrent modification exception)
190                                Set<BioPAXElement> values = new HashSet<BioPAXElement>((Set<BioPAXElement>) editor.getValueFromBean(source));
191                                if(keep == source) //i.e., it has been just added to the target, - simply update properties
192                                {
193                                        for (BioPAXElement value : values) {
194                                                migrateToTarget(source, target, editor, value);
195                                        }
196                                } else //source is normally to be entirely replaced, but if it passes the filter,
197                                        if(mergeObjPropOf!=null && mergeObjPropOf.filter(source)
198                                                && editor.isMultipleCardinality()) //and the prop. is multi-cardinality,
199                                {
200                                        //then we want to copy some values
201                                        for (BioPAXElement value : values) {
202                                                mergeToTarget(keep, target, editor, value);
203                                        }
204                                }
205
206                        } else { //primitive or enum property
207                                //primitive, enum, or string property editor (e.g., comment, name)
208                                if (mergeObjPropOf != null && mergeObjPropOf.filter(source) && editor.isMultipleCardinality()) {
209                                        Set<Object> values = new HashSet<Object>(
210                                                        (Set<Object>) editor.getValueFromBean(source));
211                                        for (Object value : values) {
212                                                mergeToTarget(keep, target, editor, value);
213                                        }
214                                }
215                        }
216                }
217        }
218        
219
220        private void migrateToTarget(BioPAXElement source, Model target, PropertyEditor editor, BioPAXElement value)
221        {
222                if (value != null)
223                {
224                        BioPAXElement newValue = target.getByID(value.getRDFId());
225                        //not null at this point, because every source element was found 
226                        //and either added to the target model, or target had an object with the same URI (to replace this value).
227                        assert newValue != null : "'newValue' is null (a design flaw in the 'merge' method)";           
228                        if (newValue != value) { //not using 'equals' intentionally
229                                editor.removeValueFromBean(value, source);
230                                editor.setValueToBean(newValue, source);
231                        } 
232                }
233        }
234        
235        private void mergeToTarget(BioPAXElement targetElement, Model target, PropertyEditor editor, Object value)
236        {
237                if (value != null) {
238                        Object newValue = (value instanceof BioPAXElement) 
239                                        ? target.getByID(((BioPAXElement)value).getRDFId()) : value;
240                        editor.setValueToBean(newValue, targetElement);
241                }
242        }
243
244}