Skip to main content
summaryrefslogtreecommitdiffstats
blob: 09dd6e91b7eff0443fdc7c91e2547fde47949ef7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
/*******************************************************************************
 * Copyright (c) 2012 Obeo.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Obeo - initial API and implementation
 *******************************************************************************/
package org.eclipse.emf.compare.match.eobject.internal;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;

import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.match.eobject.EObjectIndex;
import org.eclipse.emf.compare.match.eobject.ProximityEObjectMatcher;
import org.eclipse.emf.compare.match.eobject.ScopeQuery;
import org.eclipse.emf.ecore.EObject;

/**
 * This class is responsible for holding several version of EObjects from sides left, right or origins and
 * provides the ability to return the closest instance (from a difference side) of a given EObject. The
 * implementation expects that the queried EObjects have all been indexed before a query is done. The
 * implementation also expects that when you're done with an EObject and if you don't want to get it back in
 * the result of subsequent queries, the EObject is removed. <br>
 * The scalability of this class will highly depend on the given distance function, especially with calls
 * having a max distance of 0. <br>
 * It is also based on the assumption that it is <b> very likely that the EObject has another version with e
 * distance of value 0</b> in the other versions sets.
 * 
 * @author <a href="mailto:cedric.brun@obeo.fr">Cedric Brun</a>
 */
public class ProximityIndex implements EObjectIndex, MatchAheadOfTime {
	/**
	 * The number of elements until which the index will provide elements to match ahead of time. This is done
	 * to avoid algorithm complexity explosion. This is clearly a tradeoff as matching ahead of time means
	 * some "better match" might not be found.
	 */
	private static final int SEARCH_WINDOW = 1000;

	/**
	 * The distance function used to compare the Objects.
	 */
	private ProximityEObjectMatcher.DistanceFunction meter;

	/**
	 * the left objects still present in the index.
	 */
	private Set<EObject> lefts;

	/**
	 * the right objects still present in the index.
	 */
	private Set<EObject> rights;

	/**
	 * the origin objects still present in the index.
	 */
	private Set<EObject> origins;

	/**
	 * An object able to tell us whether an object is in the scope or not.
	 */
	private ScopeQuery scope;

	/**
	 * An object to gather statistics while matching.
	 */
	private ProximityMatchStats stats = new ProximityMatchStats();

	/**
	 * Create a new {@link ProximityIndex} using the given distance function.
	 * 
	 * @param meter
	 *            the distance function to use to compare the EObjects.
	 * @param matcher
	 *            the object used to know if an instance is in the scope or not.
	 */
	public ProximityIndex(ProximityEObjectMatcher.DistanceFunction meter, ScopeQuery matcher) {
		this.meter = meter;
		this.lefts = Sets.newLinkedHashSet();
		this.rights = Sets.newLinkedHashSet();
		this.origins = Sets.newLinkedHashSet();
		this.scope = matcher;
	}

	/**
	 * {@inheritDoc}
	 */

	public Map<Side, EObject> findClosests(Comparison inProgress, EObject eObj, Side passedObjectSide) {
		if (!readyForThisTest(inProgress, eObj)) {
			return null;
		}
		Map<Side, EObject> result = new HashMap<EObjectIndex.Side, EObject>(3);
		result.put(passedObjectSide, eObj);
		if (passedObjectSide == Side.LEFT) {
			EObject closestRight = findTheClosest(inProgress, eObj, Side.LEFT, Side.RIGHT, true);
			EObject closestOrigin = findTheClosest(inProgress, eObj, Side.LEFT, Side.ORIGIN, true);
			result.put(Side.RIGHT, closestRight);
			result.put(Side.ORIGIN, closestOrigin);
		} else if (passedObjectSide == Side.RIGHT) {
			EObject closestLeft = findTheClosest(inProgress, eObj, Side.RIGHT, Side.LEFT, true);
			EObject closestOrigin = findTheClosest(inProgress, eObj, Side.RIGHT, Side.ORIGIN, true);
			result.put(Side.LEFT, closestLeft);
			result.put(Side.ORIGIN, closestOrigin);

		} else if (passedObjectSide == Side.ORIGIN) {
			EObject closestLeft = findTheClosest(inProgress, eObj, Side.ORIGIN, Side.LEFT, true);
			EObject closestRight = findTheClosest(inProgress, eObj, Side.ORIGIN, Side.RIGHT, true);
			result.put(Side.LEFT, closestLeft);
			result.put(Side.RIGHT, closestRight);
		}

		return result;

	}

	/**
	 * return the closest EObject of the passed one found in the sideToFind storage.
	 * 
	 * @param inProgress
	 *            the comparison under match.
	 * @param eObj
	 *            the base EObject.
	 * @param originalSide
	 *            the side of the base EObject.
	 * @param sideToFind
	 *            the side to search in.
	 * @param shouldDoubleCheck
	 *            true if we should make sure that the found EObject has the inverse relationship with the
	 *            base one.
	 * @return the closest EObject of the passed one found in the sideToFind storage.
	 */
	private EObject findTheClosest(Comparison inProgress, final EObject eObj, final Side originalSide,
			final Side sideToFind, boolean shouldDoubleCheck) {
		Set<EObject> storageToSearchFor = lefts;
		switch (sideToFind) {
			case RIGHT:
				storageToSearchFor = rights;
				break;
			case LEFT:
				storageToSearchFor = lefts;
				break;
			case ORIGIN:
				storageToSearchFor = origins;
				break;

			default:
				break;
		}
		/*
		 * We are starting by looking for EObject having a distance of 0. It means we'll iterate two times in
		 * the worst case but it is very likely that the EObject has another version with a distance of 0. It
		 * is also based on the assumption that calling distance() with a 0 max distance triggers shortcuts
		 * and is faster than calling the same distance() method with a max_distance > 0.
		 */
		Candidate best = findIdenticMatch(inProgress, eObj, storageToSearchFor);
		if (best.some()) {
			return best.eObject;
		}

		SortedMap<Double, EObject> candidates = Maps.newTreeMap();
		/*
		 * We could not find an EObject which is identical, let's search again and find the closest EObject.
		 */
		Iterator<EObject> it = storageToSearchFor.iterator();
		while (best.distance != 0 && it.hasNext()) {
			EObject potentialClosest = it.next();
			double dist = meter.distance(inProgress, eObj, potentialClosest);
			stats.similarityCompare();
			if (dist < best.distance) {
				if (shouldDoubleCheck) {
					// We need to double check the currentlyDigging has the same object as the closest !
					candidates.put(Double.valueOf(dist), potentialClosest);
				} else {
					best.distance = dist;
					best.eObject = potentialClosest;
				}
			}
		}
		if (shouldDoubleCheck) {
			for (Entry<Double, EObject> entry : candidates.entrySet()) {
				EObject doubleCheck = findTheClosest(inProgress, entry.getValue(), sideToFind, originalSide,
						false);
				stats.doubleCheck();
				if (doubleCheck == eObj) {
					stats.similaritySuccess();
					best.eObject = entry.getValue();
					best.distance = entry.getKey().doubleValue();
					break;
				} else {
					stats.failedDoubleCheck();
				}
			}
		}

		if (!best.some()) {
			stats.noMatch();
		}
		return best.eObject;
	}

	/**
	 * Look for a perfect match (identic content) in the given list of candidates.
	 * 
	 * @param inProgress
	 *            the comparison being matched.
	 * @param eObj
	 *            the object
	 * @param candidates
	 *            the list of possible matches.
	 * @return a candidate instance wrapping the found match Object (if found)
	 */
	private Candidate findIdenticMatch(Comparison inProgress, final EObject eObj, Set<EObject> candidates) {
		Iterator<EObject> it = candidates.iterator();
		Candidate best = new Candidate();

		while (it.hasNext() && !best.some()) {
			EObject fastCheck = it.next();
			if (!readyForThisTest(inProgress, fastCheck)) {
				stats.backtrack();
			} else {
				stats.identicCompare();
				if (meter.areIdentic(inProgress, eObj, fastCheck)) {
					stats.identicSuccess();
					best.eObject = fastCheck;
					best.distance = 0;
				}
			}
		}
		return best;
	}

	/**
	 * Check whether we have all the required information to search for this object matches.
	 * 
	 * @param inProgress
	 *            the Comparison which is being matched.
	 * @param fastCheck
	 *            the Object we are trying to match.
	 * @return true if we have all the required information, false otherwise.
	 */
	private boolean readyForThisTest(Comparison inProgress, EObject fastCheck) {
		EObject eContainer = fastCheck.eContainer();
		if (eContainer != null && scope.isInScope(eContainer)) {
			return inProgress.getMatch(eContainer) != null;
		}
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public void remove(EObject obj, Side side) {
		switch (side) {
			case RIGHT:
				rights.remove(obj);
				break;
			case LEFT:
				lefts.remove(obj);
				break;

			case ORIGIN:
				origins.remove(obj);
				break;

			default:
				break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void index(EObject eObject, Side side) {
		switch (side) {
			case RIGHT:
				rights.add(eObject);
				break;
			case LEFT:
				lefts.add(eObject);
				break;
			case ORIGIN:
				origins.add(eObject);
				break;

			default:
				break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Collection<EObject> getValuesStillThere(final Side side) {
		Collection<EObject> result = Collections.emptyList();
		switch (side) {
			case RIGHT:
				result = ImmutableList.copyOf(rights);
				break;
			case LEFT:
				result = ImmutableList.copyOf(lefts);
				break;
			case ORIGIN:
				result = ImmutableList.copyOf(origins);
				break;
			default:
				break;
		}
		return result;

	}

	/**
	 * A Wrapper class to keep an {@link EObject} aside with a distance.
	 * 
	 * @author <a href="mailto:cedric.brun@obeo.fr">Cedric Brun</a>
	 */
	private static class Candidate {
		/**
		 * an EObject.
		 */
		protected EObject eObject;

		/**
		 * A distance.
		 */
		protected double distance = Double.MAX_VALUE;

		/**
		 * return true of the candidate has an {@link EObject}.
		 * 
		 * @return true of the candidate has an {@link EObject}.
		 */
		public boolean some() {
			return eObject != null;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<EObject> getValuesToMatchAhead(Side left) {
		Collection<EObject> indexedValues = getValuesStillThere(left);
		if (indexedValues.size() > SEARCH_WINDOW) {
			return indexedValues;
		}
		return Collections.emptyList();
	}

}

Back to the top