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}