diff options
Diffstat (limited to 'subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java')
-rw-r--r-- | subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java | 487 |
1 files changed, 310 insertions, 177 deletions
diff --git a/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java b/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java index ed551b6e..ad203316 100644 --- a/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java +++ b/subprojects/interpreter-rete-recipes/src/main/java/tools/refinery/interpreter/rete/recipes/helper/RecipeRecognizer.java | |||
@@ -1,5 +1,6 @@ | |||
1 | /******************************************************************************* | 1 | /******************************************************************************* |
2 | * Copyright (c) 2010-2016, Gabor Bergmann, Istvan Rath and Daniel Varro | 2 | * Copyright (c) 2010-2016, Gabor Bergmann, Istvan Rath and Daniel Varro |
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * This program and the accompanying materials are made available under the | 4 | * This program and the accompanying materials are made available under the |
4 | * terms of the Eclipse Public License v. 2.0 which is available at | 5 | * terms of the Eclipse Public License v. 2.0 which is available at |
5 | * http://www.eclipse.org/legal/epl-v20.html. | 6 | * http://www.eclipse.org/legal/epl-v20.html. |
@@ -9,16 +10,15 @@ | |||
9 | package tools.refinery.interpreter.rete.recipes.helper; | 10 | package tools.refinery.interpreter.rete.recipes.helper; |
10 | 11 | ||
11 | import org.eclipse.emf.common.util.EList; | 12 | import org.eclipse.emf.common.util.EList; |
12 | import org.eclipse.emf.ecore.EAttribute; | 13 | import org.eclipse.emf.ecore.*; |
13 | import org.eclipse.emf.ecore.EClass; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.EStructuralFeature; | ||
16 | import org.eclipse.emf.ecore.util.EcoreUtil; | 14 | import org.eclipse.emf.ecore.util.EcoreUtil; |
15 | import org.jetbrains.annotations.Nullable; | ||
17 | import tools.refinery.interpreter.rete.recipes.RecipesPackage; | 16 | import tools.refinery.interpreter.rete.recipes.RecipesPackage; |
18 | import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe; | 17 | import tools.refinery.interpreter.rete.recipes.ReteNodeRecipe; |
19 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; | 18 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; |
20 | 19 | ||
21 | import java.util.*; | 20 | import java.util.*; |
21 | import java.util.concurrent.atomic.AtomicLong; | ||
22 | 22 | ||
23 | /** | 23 | /** |
24 | * Stores a set of known <em>canonical</em> recipes, each representing a disjoint equivalence class of recipes, modulo | 24 | * Stores a set of known <em>canonical</em> recipes, each representing a disjoint equivalence class of recipes, modulo |
@@ -26,179 +26,312 @@ import java.util.*; | |||
26 | * | 26 | * |
27 | * @author Gabor Bergmann | 27 | * @author Gabor Bergmann |
28 | * @since 1.3 | 28 | * @since 1.3 |
29 | * | ||
30 | */ | 29 | */ |
31 | public class RecipeRecognizer { | 30 | public class RecipeRecognizer { |
32 | private static long nextRecipeEquivalenceClassID = 0; | 31 | private static final AtomicLong nextRecipeEquivalenceClassID = new AtomicLong(0); |
33 | 32 | ||
34 | /** | 33 | /** |
35 | * if EcoreUtil.equals(recipe1, recipe2), only one of them will be included here | 34 | * if EcoreUtil.equals(recipe1, recipe2), only one of them will be included here |
36 | */ | 35 | */ |
37 | Map<EClass, Set<ReteNodeRecipe>> canonicalRecipesByClass = new HashMap<>(); | 36 | Map<Long, Set<ReteNodeRecipe>> canonicalRecipesByHashCode = new HashMap<>(); |
38 | Map<Long, ReteNodeRecipe> canonicalRecipeByEquivalenceClassID = new HashMap<>(); | 37 | Map<Long, ReteNodeRecipe> canonicalRecipeByEquivalenceClassID = new HashMap<>(); |
39 | 38 | Set<ReteNodeRecipe> canonicalRecipesWithoutHashCode = new LinkedHashSet<>(); | |
40 | private IQueryRuntimeContext runtimeContext; | 39 | |
41 | 40 | private final IQueryRuntimeContext runtimeContext; | |
42 | /** | 41 | |
43 | * @param can be null; if provided, further equivalences can be detected based on {@link IQueryRuntimeContext#wrapElement(Object)} | 42 | /** |
44 | * @since 1.6 | 43 | * @param runtimeContext can be null; if provided, further equivalences can be detected based on |
45 | */ | 44 | * {@link IQueryRuntimeContext#wrapElement(Object)} |
46 | public RecipeRecognizer(IQueryRuntimeContext runtimeContext) { | 45 | * @since 1.6 |
47 | this.runtimeContext = runtimeContext; | 46 | */ |
48 | } | 47 | public RecipeRecognizer(IQueryRuntimeContext runtimeContext) { |
49 | public RecipeRecognizer() { | 48 | this.runtimeContext = runtimeContext; |
50 | this(null); | 49 | } |
51 | } | 50 | |
52 | 51 | public RecipeRecognizer() { | |
53 | /** | 52 | this(null); |
54 | * Recognizes when an equivalent canonical recipe is already known. | 53 | } |
55 | * | 54 | |
56 | * @return an equivalent canonical recipe, or the null if no known equivalent found | 55 | /** |
57 | */ | 56 | * Recognizes when an equivalent canonical recipe is already known. |
58 | public ReteNodeRecipe peekCanonicalRecipe(final ReteNodeRecipe recipe) { | 57 | * |
59 | // equivalence class already known | 58 | * @return an equivalent canonical recipe, or the null if no known equivalent found |
60 | for (Long classID : recipe.getEquivalenceClassIDs()) { | 59 | */ |
61 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(classID); | 60 | public ReteNodeRecipe peekCanonicalRecipe(final ReteNodeRecipe recipe) { |
62 | if (knownRecipe != null) | 61 | var recipeByEquivalenceClass = getRecipeByEquivalenceClass(recipe); |
63 | return knownRecipe; | 62 | if (recipeByEquivalenceClass != null) { |
64 | } | 63 | return recipeByEquivalenceClass; |
65 | 64 | } | |
66 | // equivalence class not known, but maybe equivalent recipe still | 65 | |
67 | // available | 66 | var hashCode = computeHashCode(recipe); |
68 | Collection<ReteNodeRecipe> sameClassRecipes = getSameClassCanonicalRecipes(recipe); | 67 | if (hashCode != null) { |
69 | for (ReteNodeRecipe knownRecipe : sameClassRecipes) { | 68 | var recipeByHashCode = getRecipeByHashCode(recipe, hashCode); |
70 | if (isEquivalentRecipe(recipe, knownRecipe)) { | 69 | if (recipeByHashCode != null) { |
71 | // FOUND EQUIVALENT RECIPE | 70 | return recipeByHashCode; |
72 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); | 71 | } |
73 | return knownRecipe; | 72 | } |
74 | } | 73 | |
75 | } | 74 | // If we already designated {@code recipe} as canonical during a recursive call in {@code computeHashCode}, |
76 | 75 | // it will be found here, and we will move it to {@code canonicalRecipesByHashCode}. This could be improved by | |
77 | return null; | 76 | // checking whether {@code recipe} is already canonical explicitly if there are many recursive patterns. |
78 | } | 77 | return getRecipeAndAssignHashCode(recipe, hashCode); |
79 | 78 | } | |
80 | /** | 79 | |
81 | * This recipe will be remembered as a canonical recipe. Method maintains both internal data structures and the | 80 | @Nullable |
82 | * equivalence class attribute of the recipe. PRECONDITION: {@link #peekCanonicalRecipe(ReteNodeRecipe)} must return | 81 | private ReteNodeRecipe getRecipeByEquivalenceClass(ReteNodeRecipe recipe) { |
83 | * null or the recipe itself | 82 | for (Long classID : recipe.getEquivalenceClassIDs()) { |
84 | */ | 83 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(classID); |
85 | public void makeCanonical(final ReteNodeRecipe recipe) { | 84 | if (knownRecipe != null) { |
86 | // this is a canonical recipe, chosen representative of its new | 85 | return knownRecipe; |
87 | // equivalence class | 86 | } |
88 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | 87 | } |
89 | recipe.getEquivalenceClassIDs().add(nextRecipeEquivalenceClassID++); | 88 | return null; |
90 | } | 89 | } |
91 | for (Long classID : recipe.getEquivalenceClassIDs()) { | 90 | |
92 | canonicalRecipeByEquivalenceClassID.put(classID, recipe); | 91 | @Nullable |
93 | } | 92 | private ReteNodeRecipe getRecipeByHashCode(ReteNodeRecipe recipe, Long hashCode) { |
94 | getSameClassCanonicalRecipes(recipe).add(recipe); | 93 | var equivalentRecipesByHashCode = canonicalRecipesByHashCode.get(hashCode); |
95 | } | 94 | if (equivalentRecipesByHashCode != null) { |
96 | 95 | for (ReteNodeRecipe knownRecipe : equivalentRecipesByHashCode) { | |
97 | /** | 96 | if (isEquivalentRecipe(recipe, knownRecipe)) { |
98 | * Ensures that there is an equivalent canonical recipe; if none is known yet, this recipe will be remembered as | 97 | // FOUND EQUIVALENT RECIPE |
99 | * canonical. | 98 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); |
100 | * | 99 | return knownRecipe; |
101 | * @return an equivalent canonical recipe; the argument recipe itself (which is made canonical) if no known | 100 | } |
102 | * equivalent found | 101 | } |
103 | */ | 102 | } |
104 | public ReteNodeRecipe canonicalizeRecipe(final ReteNodeRecipe recipe) { | 103 | return null; |
105 | ReteNodeRecipe knownRecipe = peekCanonicalRecipe(recipe); | 104 | } |
106 | if (knownRecipe == null) { | 105 | |
107 | knownRecipe = recipe; | 106 | @Nullable |
108 | makeCanonical(recipe); | 107 | private ReteNodeRecipe getRecipeAndAssignHashCode(ReteNodeRecipe recipe, Long hashCode) { |
109 | } | 108 | var iterator = canonicalRecipesWithoutHashCode.iterator(); |
110 | return knownRecipe; | 109 | while (iterator.hasNext()) { |
111 | } | 110 | var knownRecipe = iterator.next(); |
112 | 111 | if (isEquivalentRecipe(recipe, knownRecipe)) { | |
113 | /** | 112 | // FOUND EQUIVALENT RECIPE |
114 | * @return true iff recipe is a canonical recipe | 113 | recipe.getEquivalenceClassIDs().add(knownRecipe.getEquivalenceClassIDs().get(0)); |
115 | */ | 114 | var cachedHashCode = knownRecipe.getCachedHashCode(); |
116 | public boolean isKnownCanonicalRecipe(final ReteNodeRecipe recipe) { | 115 | if (cachedHashCode != null && !cachedHashCode.equals(hashCode)) { |
117 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | 116 | throw new AssertionError("Cached recipe %s already had hash code %s" |
118 | return false; | 117 | .formatted(knownRecipe, cachedHashCode)); |
119 | } | 118 | } |
120 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(recipe.getEquivalenceClassIDs().get(0)); | 119 | if (hashCode != null) { |
121 | return recipe == knownRecipe; | 120 | knownRecipe.setCachedHashCode(hashCode); |
122 | } | 121 | addHashCodeRepresentative(hashCode, knownRecipe); |
123 | 122 | iterator.remove(); | |
124 | private Set<ReteNodeRecipe> getSameClassCanonicalRecipes(final ReteNodeRecipe recipe) { | 123 | } |
125 | Set<ReteNodeRecipe> sameClassRecipes = canonicalRecipesByClass.get(recipe.eClass()); | 124 | return knownRecipe; |
126 | if (sameClassRecipes == null) { | 125 | } |
127 | sameClassRecipes = new HashSet<>(); | 126 | } |
128 | canonicalRecipesByClass.put(recipe.eClass(), sameClassRecipes); | 127 | return null; |
129 | } | 128 | } |
130 | return sameClassRecipes; | 129 | |
131 | } | 130 | private final Deque<EObject> hashCodeStack = new ArrayDeque<>(); |
132 | 131 | ||
133 | private boolean isEquivalentRecipe(ReteNodeRecipe recipe, ReteNodeRecipe knownRecipe) { | 132 | private Long computeHashCode(Object object) { |
134 | return new EqualityHelper(runtimeContext).equals(recipe, knownRecipe); | 133 | if (object instanceof List<?> list) { |
135 | } | 134 | return computeListHashCode(list); |
136 | 135 | } | |
137 | // TODO reuse in more cases later, e.g. switching join node parents, etc. | 136 | if (object instanceof ReteNodeRecipe recipe) { |
138 | private static class EqualityHelper extends EcoreUtil.EqualityHelper { | 137 | return ensureRecipeHashCode(recipe); |
139 | 138 | } | |
140 | 139 | if (object instanceof EObject eObject) { | |
141 | 140 | return computeEObjectHashCode(eObject); | |
142 | 141 | } | |
143 | private static final long serialVersionUID = -8841971394686015188L; | 142 | return (long) Objects.hashCode(object); |
144 | 143 | } | |
145 | private static final EAttribute RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS = | 144 | |
146 | RecipesPackage.eINSTANCE.getReteNodeRecipe_EquivalenceClassIDs(); | 145 | private Long computeHashCodeOrEquivalenceClassId(Object object) { |
147 | private static final EAttribute CONSTANT_RECIPE_CONSTANT_VALUES = | 146 | if (object instanceof ReteNodeRecipe recipe) { |
148 | RecipesPackage.eINSTANCE.getConstantRecipe_ConstantValues(); | 147 | var equivalenceClassIDs = recipe.getEquivalenceClassIDs(); |
149 | private static final EAttribute DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY = | 148 | if (!equivalenceClassIDs.isEmpty()) { |
150 | RecipesPackage.eINSTANCE.getDiscriminatorBucketRecipe_BucketKey(); | 149 | return equivalenceClassIDs.get(0); |
151 | 150 | } | |
152 | private IQueryRuntimeContext runtimeContext; | 151 | if (hashCodeStack.contains(recipe)) { |
153 | 152 | return null; | |
154 | public EqualityHelper(IQueryRuntimeContext runtimeContext) { | 153 | } |
155 | this.runtimeContext = runtimeContext; | 154 | var canonicalRecipe = canonicalizeRecipe(recipe); |
156 | } | 155 | return canonicalRecipe.getEquivalenceClassIDs().get(0); |
157 | 156 | } else { | |
158 | @Override | 157 | return computeHashCode(object); |
159 | protected boolean haveEqualFeature(EObject eObject1, EObject eObject2, EStructuralFeature feature) { | 158 | } |
160 | // ignore differences in this attribute, as it may only be assigned | 159 | } |
161 | // after the equivalence check | 160 | |
162 | if (RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS.equals(feature)) | 161 | private Long computeListHashCode(List<?> list) { |
163 | return true; | 162 | long result = 1; |
164 | 163 | for (var item : list) { | |
165 | if (runtimeContext != null) { | 164 | var update = computeHashCodeOrEquivalenceClassId(item); |
166 | // constant values | 165 | if (update == null) { |
167 | if (/*CONSTANT_RECIPE_CONSTANT_VALUES.equals(feature) ||*/ DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY.equals(feature)) { | 166 | return null; |
168 | // use runtime context to map to canonical wrapped form | 167 | } |
169 | // this is costly for constant recipes (TODO improve this), but essential for discriminator buckets | 168 | result = result * 37 + update; |
170 | 169 | } | |
171 | Object val1 = eObject1.eGet(feature); | 170 | return result; |
172 | Object val2 = eObject2.eGet(feature); | 171 | } |
173 | 172 | ||
174 | if (val1 != null && val2 != null) { | 173 | private Long ensureRecipeHashCode(ReteNodeRecipe recipe) { |
175 | return runtimeContext.wrapElement(val1).equals(runtimeContext.wrapElement(val2)); | 174 | var hashCode = recipe.getCachedHashCode(); |
176 | } else { | 175 | if (hashCode != null) { |
177 | return val1 == null && val2 == null; | 176 | return hashCode; |
178 | } | 177 | } |
179 | 178 | hashCode = computeEObjectHashCode(recipe); | |
180 | } | 179 | if (hashCode == null) { |
181 | } | 180 | return null; |
182 | 181 | } | |
183 | // fallback to general comparison | 182 | recipe.setCachedHashCode(hashCode); |
184 | return super.haveEqualFeature(eObject1, eObject2, feature); | 183 | return hashCode; |
185 | } | 184 | } |
186 | 185 | ||
187 | @Override | 186 | private Long computeEObjectHashCode(EObject eObject) { |
188 | public boolean equals(EObject eObject1, EObject eObject2) { | 187 | if (hashCodeStack.contains(eObject)) { |
189 | // short-circuit if already known to be equivalent | 188 | return null; |
190 | if (eObject1 instanceof ReteNodeRecipe) { | 189 | } |
191 | if (eObject2 instanceof ReteNodeRecipe) { | 190 | hashCodeStack.addLast(eObject); |
192 | EList<Long> eqClassIDs1 = ((ReteNodeRecipe) eObject1).getEquivalenceClassIDs(); | 191 | try { |
193 | EList<Long> eqClassIDs2 = ((ReteNodeRecipe) eObject2).getEquivalenceClassIDs(); | 192 | long result = eObject.eClass().hashCode(); |
194 | 193 | for (var feature : eObject.eClass().getEAllStructuralFeatures()) { | |
195 | if (!Collections.disjoint(eqClassIDs1, eqClassIDs2)) | 194 | if (eObject instanceof ReteNodeRecipe && ( |
196 | return true; | 195 | RecipesPackage.Literals.RETE_NODE_RECIPE__EQUIVALENCE_CLASS_IDS.equals(feature) || |
197 | } | 196 | RecipesPackage.Literals.RETE_NODE_RECIPE__CACHED_HASH_CODE.equals(feature) || |
198 | } | 197 | RecipesPackage.Literals.RETE_NODE_RECIPE__CONSTRUCTED.equals(feature))) { |
199 | 198 | continue; | |
200 | // fallback to general comparison | 199 | } |
201 | return super.equals(eObject1, eObject2); | 200 | var value = eObject.eGet(feature); |
202 | } | 201 | var update = computeHashCodeOrEquivalenceClassId(value); |
203 | } | 202 | if (update == null) { |
203 | return null; | ||
204 | } | ||
205 | result = result * 37 + update; | ||
206 | } | ||
207 | return result; | ||
208 | } finally { | ||
209 | hashCodeStack.removeLast(); | ||
210 | } | ||
211 | } | ||
212 | |||
213 | private void addHashCodeRepresentative(Long hashCode, ReteNodeRecipe recipe) { | ||
214 | canonicalRecipesByHashCode.computeIfAbsent(hashCode, ignored -> new LinkedHashSet<>()).add(recipe); | ||
215 | } | ||
216 | |||
217 | /** | ||
218 | * This recipe will be remembered as a canonical recipe. Method maintains both internal data structures and the | ||
219 | * equivalence class attribute of the recipe. PRECONDITION: {@link #peekCanonicalRecipe(ReteNodeRecipe)} must | ||
220 | * return null or the recipe itself | ||
221 | */ | ||
222 | public void makeCanonical(final ReteNodeRecipe recipe) { | ||
223 | // this is a canonical recipe, chosen representative of its new | ||
224 | // equivalence class | ||
225 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | ||
226 | recipe.getEquivalenceClassIDs().add(nextRecipeEquivalenceClassID.getAndIncrement()); | ||
227 | } | ||
228 | for (Long classID : recipe.getEquivalenceClassIDs()) { | ||
229 | canonicalRecipeByEquivalenceClassID.put(classID, recipe); | ||
230 | } | ||
231 | var hashCode = computeHashCode(recipe); | ||
232 | if (hashCode == null) { | ||
233 | canonicalRecipesWithoutHashCode.add(recipe); | ||
234 | } else { | ||
235 | addHashCodeRepresentative(hashCode, recipe); | ||
236 | } | ||
237 | } | ||
238 | |||
239 | /** | ||
240 | * Ensures that there is an equivalent canonical recipe; if none is known yet, this recipe will be remembered as | ||
241 | * canonical. | ||
242 | * | ||
243 | * @return an equivalent canonical recipe; the argument recipe itself (which is made canonical) if no known | ||
244 | * equivalent found | ||
245 | */ | ||
246 | public ReteNodeRecipe canonicalizeRecipe(final ReteNodeRecipe recipe) { | ||
247 | ReteNodeRecipe knownRecipe = peekCanonicalRecipe(recipe); | ||
248 | if (knownRecipe == null) { | ||
249 | knownRecipe = recipe; | ||
250 | makeCanonical(recipe); | ||
251 | } | ||
252 | return knownRecipe; | ||
253 | } | ||
254 | |||
255 | /** | ||
256 | * @return true iff recipe is a canonical recipe | ||
257 | */ | ||
258 | public boolean isKnownCanonicalRecipe(final ReteNodeRecipe recipe) { | ||
259 | if (recipe.getEquivalenceClassIDs().isEmpty()) { | ||
260 | return false; | ||
261 | } | ||
262 | ReteNodeRecipe knownRecipe = canonicalRecipeByEquivalenceClassID.get(recipe.getEquivalenceClassIDs().get(0)); | ||
263 | return recipe == knownRecipe; | ||
264 | } | ||
265 | |||
266 | private boolean isEquivalentRecipe(ReteNodeRecipe recipe, ReteNodeRecipe knownRecipe) { | ||
267 | return new EqualityHelper(runtimeContext).equals(recipe, knownRecipe); | ||
268 | } | ||
269 | |||
270 | // TODO reuse in more cases later, e.g. switching join node parents, etc. | ||
271 | private static class EqualityHelper extends EcoreUtil.EqualityHelper { | ||
272 | private static final long serialVersionUID = -8841971394686015188L; | ||
273 | |||
274 | private static final EAttribute RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS = | ||
275 | RecipesPackage.eINSTANCE.getReteNodeRecipe_EquivalenceClassIDs(); | ||
276 | private static final EAttribute RETE_NODE_RECIPE_CACHED_HASH_CODE = | ||
277 | RecipesPackage.eINSTANCE.getReteNodeRecipe_CachedHashCode(); | ||
278 | private static final EAttribute RETE_NODE_RECIPE_CONSTRUCTED = | ||
279 | RecipesPackage.eINSTANCE.getReteNodeRecipe_Constructed(); | ||
280 | private static final EAttribute DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY = | ||
281 | RecipesPackage.eINSTANCE.getDiscriminatorBucketRecipe_BucketKey(); | ||
282 | |||
283 | private final IQueryRuntimeContext runtimeContext; | ||
284 | |||
285 | public EqualityHelper(IQueryRuntimeContext runtimeContext) { | ||
286 | this.runtimeContext = runtimeContext; | ||
287 | } | ||
288 | |||
289 | @Override | ||
290 | protected boolean haveEqualFeature(EObject eObject1, EObject eObject2, EStructuralFeature feature) { | ||
291 | // ignore differences in this attribute, as it may only be assigned | ||
292 | // after the equivalence check | ||
293 | if (RETE_NODE_RECIPE_EQUIVALENCE_CLASS_IDS.equals(feature) || | ||
294 | RETE_NODE_RECIPE_CACHED_HASH_CODE.equals(feature) || | ||
295 | RETE_NODE_RECIPE_CONSTRUCTED.equals(feature)) | ||
296 | return true; | ||
297 | |||
298 | if (runtimeContext != null) { | ||
299 | // constant values | ||
300 | if (DISCRIMINATOR_BUCKET_RECIPE_BUCKET_KEY.equals(feature)) { | ||
301 | // use runtime context to map to canonical wrapped form | ||
302 | // this is costly for constant recipes (TODO improve this), but essential for discriminator buckets | ||
303 | |||
304 | Object val1 = eObject1.eGet(feature); | ||
305 | Object val2 = eObject2.eGet(feature); | ||
306 | |||
307 | if (val1 != null && val2 != null) { | ||
308 | return runtimeContext.wrapElement(val1).equals(runtimeContext.wrapElement(val2)); | ||
309 | } else { | ||
310 | return val1 == null && val2 == null; | ||
311 | } | ||
312 | |||
313 | } | ||
314 | } | ||
315 | |||
316 | // fallback to general comparison | ||
317 | return super.haveEqualFeature(eObject1, eObject2, feature); | ||
318 | } | ||
319 | |||
320 | @Override | ||
321 | public boolean equals(EObject eObject1, EObject eObject2) { | ||
322 | // short-circuit if already known to be equivalent | ||
323 | if (eObject1 instanceof ReteNodeRecipe) { | ||
324 | if (eObject2 instanceof ReteNodeRecipe) { | ||
325 | EList<Long> eqClassIDs1 = ((ReteNodeRecipe) eObject1).getEquivalenceClassIDs(); | ||
326 | EList<Long> eqClassIDs2 = ((ReteNodeRecipe) eObject2).getEquivalenceClassIDs(); | ||
327 | |||
328 | if (!Collections.disjoint(eqClassIDs1, eqClassIDs2)) | ||
329 | return true; | ||
330 | } | ||
331 | } | ||
332 | |||
333 | // fallback to general comparison | ||
334 | return super.equals(eObject1, eObject2); | ||
335 | } | ||
336 | } | ||
204 | } | 337 | } |