Skip to content

Commit a33e867

Browse files
committed
LUCENE-2188: Add a utility class for tracking deprecated overridden methods in non-final subclasses
git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@898507 13f79535-47bb-0310-9956-ffa450edef68
1 parent eaed8e6 commit a33e867

File tree

8 files changed

+270
-21
lines changed

8 files changed

+270
-21
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ Optimizations
166166
directly, instead of Byte/CharBuffers, and modify CollationKeyFilter to
167167
take advantage of this for faster performance.
168168
(Steven Rowe, Uwe Schindler, Robert Muir)
169+
170+
* LUCENE-2188: Add a utility class for tracking deprecated overridden
171+
methods in non-final subclasses.
172+
(Uwe Schindler, Robert Muir)
169173

170174
Build
171175

src/java/org/apache/lucene/analysis/Analyzer.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
import java.io.Reader;
2121
import java.io.IOException;
2222
import java.io.Closeable;
23-
import java.lang.reflect.Method;
2423

2524
import org.apache.lucene.util.CloseableThreadLocal;
25+
import org.apache.lucene.util.VirtualMethod;
2626
import org.apache.lucene.store.AlreadyClosedException;
2727

2828
import org.apache.lucene.document.Fieldable;
@@ -84,22 +84,25 @@ protected void setPreviousTokenStream(Object obj) {
8484
}
8585
}
8686

87-
/** @deprecated */
87+
private static final VirtualMethod<Analyzer> tokenStreamMethod =
88+
new VirtualMethod<Analyzer>(Analyzer.class, "tokenStream", String.class, Reader.class);
89+
private static final VirtualMethod<Analyzer> reusableTokenStreamMethod =
90+
new VirtualMethod<Analyzer>(Analyzer.class, "reusableTokenStream", String.class, Reader.class);
91+
92+
/** This field contains if the {@link #tokenStream} method was overridden in a
93+
* more far away subclass of {@code Analyzer} on the current instance's inheritance path.
94+
* If this field is {@code true}, {@link #reusableTokenStream} should delegate to {@link #tokenStream}
95+
* instead of using the own implementation.
96+
* @deprecated Please declare all implementations of {@link #reusableTokenStream} and {@link #tokenStream}
97+
* as {@code final}.
98+
*/
8899
@Deprecated
89-
protected boolean overridesTokenStreamMethod = false;
100+
protected final boolean overridesTokenStreamMethod =
101+
VirtualMethod.compareImplementationDistance(this.getClass(), tokenStreamMethod, reusableTokenStreamMethod) > 0;
90102

91-
/** @deprecated This is only present to preserve
92-
* back-compat of classes that subclass a core analyzer
93-
* and override tokenStream but not reusableTokenStream */
103+
/** @deprecated This is a no-op since Lucene 3.1. */
94104
@Deprecated
95105
protected void setOverridesTokenStreamMethod(Class<? extends Analyzer> baseClass) {
96-
try {
97-
Method m = this.getClass().getMethod("tokenStream", String.class, Reader.class);
98-
overridesTokenStreamMethod = m.getDeclaringClass() != baseClass;
99-
} catch (NoSuchMethodException nsme) {
100-
// cannot happen, as baseClass is subclass of Analyzer through generics
101-
overridesTokenStreamMethod = false;
102-
}
103106
}
104107

105108

src/java/org/apache/lucene/analysis/KeywordAnalyzer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
*/
2727
public class KeywordAnalyzer extends Analyzer {
2828
public KeywordAnalyzer() {
29-
setOverridesTokenStreamMethod(KeywordAnalyzer.class);
3029
}
3130
@Override
3231
public TokenStream tokenStream(String fieldName,

src/java/org/apache/lucene/analysis/PerFieldAnalyzerWrapper.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ public PerFieldAnalyzerWrapper(Analyzer defaultAnalyzer,
7272
if (fieldAnalyzers != null) {
7373
analyzerMap.putAll(fieldAnalyzers);
7474
}
75-
setOverridesTokenStreamMethod(PerFieldAnalyzerWrapper.class);
7675
}
7776

7877

src/java/org/apache/lucene/analysis/StopwordAnalyzerBase.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ public Set<?> getStopwordSet() {
5858
* the analyzer's stopword set
5959
*/
6060
protected StopwordAnalyzerBase(final Version version, final Set<?> stopwords) {
61-
/*
62-
* no need to call
63-
* setOverridesTokenStreamMethod(StopwordAnalyzerBase.class); here, both
64-
* tokenStream methods are final in this class.
65-
*/
6661
matchVersion = version;
6762
// analyzers should use char array set for stopwords!
6863
this.stopwords = stopwords == null ? CharArraySet.EMPTY_SET : CharArraySet

src/java/org/apache/lucene/analysis/standard/StandardAnalyzer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ public StandardAnalyzer(Version matchVersion) {
7171
* @param stopWords stop words */
7272
public StandardAnalyzer(Version matchVersion, Set<?> stopWords) {
7373
stopSet = stopWords;
74-
setOverridesTokenStreamMethod(StandardAnalyzer.class);
7574
replaceInvalidAcronym = matchVersion.onOrAfter(Version.LUCENE_24);
7675
this.matchVersion = matchVersion;
7776
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package org.apache.lucene.util;
2+
3+
/**
4+
* Licensed to the Apache Software Foundation (ASF) under one or more
5+
* contributor license agreements. See the NOTICE file distributed with
6+
* this work for additional information regarding copyright ownership.
7+
* The ASF licenses this file to You under the Apache License, Version 2.0
8+
* (the "License"); you may not use this file except in compliance with
9+
* the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import java.lang.reflect.Method;
21+
import java.util.Collections;
22+
import java.util.HashSet;
23+
import java.util.IdentityHashMap;
24+
import java.util.Set;
25+
26+
/**
27+
* A utility for keeping backwards compatibility on previously abstract methods
28+
* (or similar replacements).
29+
* <p>Before the replacement method can be made abstract, the old method must kept deprecated.
30+
* If somebody still overrides the deprecated method in a non-final class,
31+
* you must keep track, of this and maybe delegate to the old method in the subclass.
32+
* The cost of reflection is minimized by the following usage of this class:</p>
33+
* <p>Define <strong>static final</strong> fields in the base class ({@code BaseClass}),
34+
* where the old and new method are declared:</p>
35+
* <pre>
36+
* static final VirtualMethod&lt;BaseClass&gt; newMethod =
37+
* new VirtualMethod&lt;BaseClass&gt;(BaseClass.class, "newName", parameters...);
38+
* static final VirtualMethod&lt;BaseClass&gt; oldMethod =
39+
* new VirtualMethod&lt;BaseClass&gt;(BaseClass.class, "oldName", parameters...);
40+
* </pre>
41+
* <p>This enforces the singleton status of these objects, as the maintenance of the cache would be too costly else.
42+
* If you try to create a second instance of for the same method/{@code baseClass} combination, an exception is thrown.
43+
* <p>To detect if e.g. the old method was overridden by a more far subclass on the inheritance path to the current
44+
* instance's class, use a <strong>non-static</strong> field:</p>
45+
* <pre>
46+
* final boolean isDeprecatedMethodOverridden =
47+
* oldMethod.getImplementationDistance(this.getClass()) > newMethod.getImplementationDistance(this.getClass());
48+
*
49+
* <em>// alternatively (more readable):</em>
50+
* final boolean isDeprecatedMethodOverridden =
51+
* VirtualMethod.compareImplementationDistance(this.getClass(), oldMethod, newMethod) > 0
52+
* </pre>
53+
* <p>{@link #getImplementationDistance} returns the distance of the subclass that overrides this method.
54+
* The one with the larger distance should be used preferable.
55+
* This way also more complicated method rename scenarios can be handled
56+
* (think of 2.9 {@code TokenStream} deprecations).</p>
57+
*/
58+
public final class VirtualMethod<C> {
59+
60+
private static final Set<Method> singletonSet = Collections.synchronizedSet(new HashSet<Method>());
61+
62+
private final Class<C> baseClass;
63+
private final String method;
64+
private final Class<?>[] parameters;
65+
private final IdentityHashMap<Class<? extends C>, Integer> cache =
66+
new IdentityHashMap<Class<? extends C>, Integer>();
67+
68+
/**
69+
* Creates a new instance for the given {@code baseClass} and method declaration.
70+
* @throws UnsupportedOperationException if you create a second instance of the same
71+
* {@code baseClass} and method declaration combination. This enforces the singleton status.
72+
* @throws IllegalArgumentException if {@code baseClass} does not declare the given method.
73+
*/
74+
public VirtualMethod(Class<C> baseClass, String method, Class<?>... parameters) {
75+
this.baseClass = baseClass;
76+
this.method = method;
77+
this.parameters = parameters;
78+
try {
79+
if (!singletonSet.add(baseClass.getDeclaredMethod(method, parameters)))
80+
throw new UnsupportedOperationException(
81+
"VirtualMethod instances must be singletons and therefore " +
82+
"assigned to static final members in the same class, they use as baseClass ctor param."
83+
);
84+
} catch (NoSuchMethodException nsme) {
85+
throw new IllegalArgumentException(baseClass.getName() + " has no such method: "+nsme.getMessage());
86+
}
87+
}
88+
89+
/**
90+
* Returns the distance from the {@code baseClass} in which this method is overridden/implemented
91+
* in the inheritance path between {@code baseClass} and the given subclass {@code subclazz}.
92+
* @return 0 iff not overridden, else the distance to the base class
93+
*/
94+
public synchronized int getImplementationDistance(final Class<? extends C> subclazz) {
95+
Integer distance = cache.get(subclazz);
96+
if (distance == null) {
97+
cache.put(subclazz, distance = Integer.valueOf(reflectImplementationDistance(subclazz)));
98+
}
99+
return distance.intValue();
100+
}
101+
102+
/**
103+
* Returns, if this method is overridden/implemented in the inheritance path between
104+
* {@code baseClass} and the given subclass {@code subclazz}.
105+
* <p>You can use this method to detect if a method that should normally be final was overridden
106+
* by the given instance's class.
107+
* @return {@code false} iff not overridden
108+
*/
109+
public boolean isOverriddenAsOf(final Class<? extends C> subclazz) {
110+
return getImplementationDistance(subclazz) > 0;
111+
}
112+
113+
private int reflectImplementationDistance(final Class<? extends C> subclazz) {
114+
if (!baseClass.isAssignableFrom(subclazz))
115+
throw new IllegalArgumentException(subclazz.getName() + " is not a subclass of " + baseClass.getName());
116+
boolean overridden = false;
117+
int distance = 0;
118+
for (Class<?> clazz = subclazz; clazz != baseClass && clazz != null; clazz = clazz.getSuperclass()) {
119+
// lookup method, if success mark as overridden
120+
if (!overridden) {
121+
try {
122+
clazz.getDeclaredMethod(method, parameters);
123+
overridden = true;
124+
} catch (NoSuchMethodException nsme) {
125+
}
126+
}
127+
128+
// increment distance if overridden
129+
if (overridden) distance++;
130+
}
131+
return distance;
132+
}
133+
134+
/**
135+
* Utility method that compares the implementation/override distance of two methods.
136+
* @return <ul>
137+
* <li>&gt; 1, iff {@code m1} is overridden/implemented in a subclass of the class overriding/declaring {@code m2}
138+
* <li>&lt; 1, iff {@code m2} is overridden in a subclass of the class overriding/declaring {@code m1}
139+
* <li>0, iff both methods are overridden in the same class (or are not overridden at all)
140+
* </ul>
141+
*/
142+
public static <C> int compareImplementationDistance(final Class<? extends C> clazz,
143+
final VirtualMethod<C> m1, final VirtualMethod<C> m2)
144+
{
145+
return Integer.valueOf(m1.getImplementationDistance(clazz)).compareTo(m2.getImplementationDistance(clazz));
146+
}
147+
148+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package org.apache.lucene.util;
2+
3+
/**
4+
* Licensed to the Apache Software Foundation (ASF) under one or more
5+
* contributor license agreements. See the NOTICE file distributed with
6+
* this work for additional information regarding copyright ownership.
7+
* The ASF licenses this file to You under the Apache License, Version 2.0
8+
* (the "License"); you may not use this file except in compliance with
9+
* the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
public class TestVirtualMethod extends LuceneTestCase {
21+
22+
private static final VirtualMethod<TestVirtualMethod> publicTestMethod =
23+
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "publicTest", String.class);
24+
private static final VirtualMethod<TestVirtualMethod> protectedTestMethod =
25+
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "protectedTest", int.class);
26+
27+
public void publicTest(String test) {}
28+
protected void protectedTest(int test) {}
29+
30+
static class TestClass1 extends TestVirtualMethod {
31+
@Override
32+
public void publicTest(String test) {}
33+
@Override
34+
protected void protectedTest(int test) {}
35+
}
36+
37+
static class TestClass2 extends TestClass1 {
38+
@Override // make it public here
39+
public void protectedTest(int test) {}
40+
}
41+
42+
static class TestClass3 extends TestClass2 {
43+
@Override
44+
public void publicTest(String test) {}
45+
}
46+
47+
static class TestClass4 extends TestVirtualMethod {
48+
}
49+
50+
static class TestClass5 extends TestClass4 {
51+
}
52+
53+
public void test() {
54+
assertEquals(0, publicTestMethod.getImplementationDistance(this.getClass()));
55+
assertEquals(1, publicTestMethod.getImplementationDistance(TestClass1.class));
56+
assertEquals(1, publicTestMethod.getImplementationDistance(TestClass2.class));
57+
assertEquals(3, publicTestMethod.getImplementationDistance(TestClass3.class));
58+
assertFalse(publicTestMethod.isOverriddenAsOf(TestClass4.class));
59+
assertFalse(publicTestMethod.isOverriddenAsOf(TestClass5.class));
60+
61+
assertEquals(0, protectedTestMethod.getImplementationDistance(this.getClass()));
62+
assertEquals(1, protectedTestMethod.getImplementationDistance(TestClass1.class));
63+
assertEquals(2, protectedTestMethod.getImplementationDistance(TestClass2.class));
64+
assertEquals(2, protectedTestMethod.getImplementationDistance(TestClass3.class));
65+
assertFalse(protectedTestMethod.isOverriddenAsOf(TestClass4.class));
66+
assertFalse(protectedTestMethod.isOverriddenAsOf(TestClass5.class));
67+
68+
assertTrue(VirtualMethod.compareImplementationDistance(TestClass3.class, publicTestMethod, protectedTestMethod) > 0);
69+
assertEquals(0, VirtualMethod.compareImplementationDistance(TestClass5.class, publicTestMethod, protectedTestMethod));
70+
71+
try {
72+
// cast to Class to remove generics:
73+
@SuppressWarnings("unchecked") int dist = publicTestMethod.getImplementationDistance((Class) LuceneTestCase.class);
74+
fail("LuceneTestCase is not a subclass and can never override publicTest(String)");
75+
} catch (IllegalArgumentException arg) {
76+
// pass
77+
}
78+
79+
try {
80+
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "bogus");
81+
fail("Method bogus() does not exist, so IAE should be thrown");
82+
} catch (IllegalArgumentException arg) {
83+
// pass
84+
}
85+
86+
try {
87+
new VirtualMethod<TestClass2>(TestClass2.class, "publicTest", String.class);
88+
fail("Method publicTest(String) is not declared in TestClass2, so IAE should be thrown");
89+
} catch (IllegalArgumentException arg) {
90+
// pass
91+
}
92+
93+
try {
94+
// try to create a second instance of the same baseClass / method combination
95+
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "publicTest", String.class);
96+
fail("Violating singleton status succeeded");
97+
} catch (UnsupportedOperationException arg) {
98+
// pass
99+
}
100+
}
101+
102+
}

0 commit comments

Comments
 (0)