-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a lightweight Dependency Injection service
- Loading branch information
Showing
6 changed files
with
358 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# BUILD FILE SYNTAX: SKYLARK | ||
|
||
java_library( | ||
name = "injector", | ||
srcs = glob(["*.java"]), | ||
deps = [ | ||
"//third_party/java/guava:guava", | ||
], | ||
visibility = [ | ||
"//java/server/test/org/openqa/selenium/injector:" | ||
], | ||
) |
164 changes: 164 additions & 0 deletions
164
java/server/src/org/openqa/selenium/injector/Injector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// Licensed to the Software Freedom Conservancy (SFC) under one | ||
// or more contributor license agreements. See the NOTICE file | ||
// distributed with this work for additional information | ||
// regarding copyright ownership. The SFC licenses this file | ||
// to you under the Apache License, Version 2.0 (the | ||
// "License"); you may not use this file except in compliance | ||
// with the License. You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package org.openqa.selenium.injector; | ||
|
||
import com.google.common.collect.ImmutableSet; | ||
|
||
import java.lang.reflect.Constructor; | ||
import java.lang.reflect.Parameter; | ||
import java.util.ArrayList; | ||
import java.util.Comparator; | ||
import java.util.HashSet; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Stream; | ||
|
||
public class Injector { | ||
|
||
private final static int MAGIC_SIZE = 100; | ||
|
||
private final Injector parent; | ||
private final ImmutableSet<Object> injectables; | ||
private final Map<Class<?>, Object> seenMappings; | ||
|
||
private Injector(Injector parent, Set<Object> injectables) { | ||
this.parent = parent; | ||
this.injectables = ImmutableSet.copyOf(injectables); | ||
|
||
// Maintain a cache of lookups to make things faster. Limit the size to stop it growing too | ||
// large and consuming all the memory. | ||
this.seenMappings = new LinkedHashMap<Class<?>, Object>() { | ||
@Override | ||
protected boolean removeEldestEntry(Map.Entry<Class<?>, Object> eldest) { | ||
return size() > MAGIC_SIZE; | ||
} | ||
}; | ||
} | ||
|
||
public static Builder builder() { | ||
return new Builder(); | ||
} | ||
|
||
public <T> T newInstance(Class<T> stereotype) { | ||
try { | ||
// Find the longest constructor | ||
class ConstructorAndArgs { | ||
public Constructor<?> constructor; | ||
public List<Object> args; | ||
|
||
public ConstructorAndArgs(Constructor<?> constructor, List<Object> args) { | ||
this.constructor = constructor; | ||
this.args = args; | ||
} | ||
} | ||
|
||
Optional<ConstructorAndArgs> possibleConstructor = | ||
Stream.of(stereotype.getDeclaredConstructors()) | ||
.map(con -> new ConstructorAndArgs(con, populateArgs(con))) | ||
.filter(canda -> canda.args != null) | ||
.max(Comparator.comparing(canda -> canda.args.size())); | ||
|
||
if (!possibleConstructor.isPresent()) { | ||
throw new UnableToInstaniateInstanceException( | ||
"Unable to find required matches for constructor of: " + stereotype); | ||
} | ||
|
||
ConstructorAndArgs canda = possibleConstructor.get(); | ||
canda.constructor.setAccessible(true); | ||
//noinspection unchecked | ||
return (T) canda.constructor.newInstance(canda.args.toArray()); | ||
} catch (ReflectiveOperationException e) { | ||
throw new UnableToInstaniateInstanceException(e); | ||
} | ||
} | ||
|
||
private List<Object> populateArgs(Constructor<?> constructor) { | ||
List<Object> toReturn = new ArrayList<>(constructor.getParameterCount()); | ||
|
||
for (Parameter param : constructor.getParameters()) { | ||
Object value = findArg(param.getType()); | ||
|
||
if (value == null) { | ||
return null; | ||
} | ||
|
||
toReturn.add(value); | ||
} | ||
|
||
return toReturn; | ||
} | ||
|
||
private Object findArg(Class<?> parameterType) { | ||
Optional<Object> possibleMatch = injectables.stream() | ||
.filter(obj -> obj.getClass().isAssignableFrom(parameterType)) | ||
.findFirst(); | ||
|
||
// Only cache items from this injector. | ||
if (possibleMatch.isPresent()) { | ||
seenMappings.put(parameterType, possibleMatch.get()); | ||
return possibleMatch.get(); | ||
} else { | ||
return parent == null ? null : parent.findArg(parameterType); | ||
} | ||
} | ||
|
||
public static class Builder { | ||
private Injector parent; | ||
private final Set<Object> registered = new HashSet<>(); | ||
private final Set<Class<?>> registeredClasses = new HashSet<>(); | ||
|
||
private Builder() { | ||
// Only accessed via builder method above | ||
} | ||
|
||
public Builder register(Object object) { | ||
Objects.requireNonNull(object); | ||
|
||
// Ensure we only add one instance of each type. | ||
if (registeredClasses.contains(object.getClass())) { | ||
throw new IllegalArgumentException(String.format( | ||
"Only one instance of a particular class is supported. Duplicate instance of %s is added: %s", | ||
object.getClass(), | ||
object)); | ||
} | ||
|
||
registered.add(object); | ||
registeredClasses.add(object.getClass()); | ||
|
||
return this; | ||
} | ||
|
||
public Builder parent(Injector parent) { | ||
if (this.parent != null) { | ||
throw new IllegalStateException("Injectors may only have one parent"); | ||
} | ||
|
||
this.parent = Objects.requireNonNull(parent); | ||
|
||
return this; | ||
} | ||
|
||
public Injector build() { | ||
return new Injector(parent, registered); | ||
} | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
java/server/src/org/openqa/selenium/injector/UnableToInstaniateInstanceException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Licensed to the Software Freedom Conservancy (SFC) under one | ||
// or more contributor license agreements. See the NOTICE file | ||
// distributed with this work for additional information | ||
// regarding copyright ownership. The SFC licenses this file | ||
// to you under the Apache License, Version 2.0 (the | ||
// "License"); you may not use this file except in compliance | ||
// with the License. You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package org.openqa.selenium.injector; | ||
|
||
public class UnableToInstaniateInstanceException extends RuntimeException { | ||
|
||
public UnableToInstaniateInstanceException(String message) { | ||
super(message); | ||
} | ||
|
||
public UnableToInstaniateInstanceException(Throwable cause) { | ||
super(cause); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# BUILD FILE SYNTAX: SKYLARK | ||
|
||
java_test( | ||
name = "small-tests", | ||
srcs = glob(["*.java"]), | ||
deps = [ | ||
"//java/client/src/org/openqa/selenium:core", | ||
"//java/client/src/org/openqa/selenium/json:json", | ||
"//java/server/src/org/openqa/selenium/injector:injector", | ||
"//third_party/java/junit:junit", | ||
], | ||
) |
140 changes: 140 additions & 0 deletions
140
java/server/test/org/openqa/selenium/injector/InjectorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Licensed to the Software Freedom Conservancy (SFC) under one | ||
// or more contributor license agreements. See the NOTICE file | ||
// distributed with this work for additional information | ||
// regarding copyright ownership. The SFC licenses this file | ||
// to you under the Apache License, Version 2.0 (the | ||
// "License"); you may not use this file except in compliance | ||
// with the License. You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package org.openqa.selenium.injector; | ||
|
||
import static org.junit.Assert.assertSame; | ||
import static org.junit.Assert.assertTrue; | ||
|
||
import org.junit.Test; | ||
import org.openqa.selenium.Proxy; | ||
import org.openqa.selenium.json.Json; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class InjectorTest { | ||
|
||
@Test | ||
public void shouldInstantiateAnObjectWithANoArgPublicConstructor() { | ||
Injector injector = Injector.builder().build(); | ||
|
||
List list = injector.newInstance(ArrayList.class); | ||
|
||
assertTrue(list instanceof ArrayList); | ||
} | ||
|
||
@Test(expected = UnableToInstaniateInstanceException.class) | ||
public void unmetConstructorParametersAreAnError() { | ||
Injector injector = Injector.builder().build(); | ||
|
||
injector.newInstance(NeedsJson.class); | ||
} | ||
|
||
@Test | ||
public void shouldUseObjectsInInjectorToPopulateNewClassViaPublicConstructor() { | ||
Json json = new Json(); | ||
Injector injector = Injector.builder().register(json).build(); | ||
|
||
NeedsJson needsJson = injector.newInstance(NeedsJson.class); | ||
|
||
assertSame(json, needsJson.json); | ||
} | ||
|
||
public static class NeedsJson { | ||
private final Json json; | ||
|
||
public NeedsJson(Json json) { | ||
this.json = json; | ||
} | ||
} | ||
|
||
@Test | ||
public void shouldUseLongestConstructor() { | ||
Json json = new Json(); | ||
Proxy proxy = new Proxy(); | ||
Injector injector = Injector.builder().register(json).register(proxy).build(); | ||
|
||
MultipleConstructors instance = injector.newInstance(MultipleConstructors.class); | ||
|
||
assertSame(json, instance.json); | ||
assertSame(proxy, instance.proxy); | ||
} | ||
|
||
public static class MultipleConstructors { | ||
|
||
private final Proxy proxy; | ||
private final Json json; | ||
|
||
public MultipleConstructors() { | ||
this(null, null); | ||
} | ||
|
||
// In the middle in case the constructors are found in declaration order. | ||
public MultipleConstructors(Proxy proxy, Json json) { | ||
this.proxy = proxy; | ||
this.json = json; | ||
} | ||
|
||
public MultipleConstructors(Json json) { | ||
this(null, json); | ||
} | ||
|
||
public MultipleConstructors(Proxy proxy) { | ||
this(proxy, null); | ||
} | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void itIsNotAllowedToInsertTwoInstancesOfTheSameClass() { | ||
Injector.builder().register("hello").register("world"); | ||
} | ||
|
||
@Test | ||
public void canFallbackToParentInjector() { | ||
Json json = new Json(); | ||
Injector parent = Injector.builder().register(json).build(); | ||
|
||
Proxy proxy = new Proxy(); | ||
Injector child = Injector.builder().parent(parent).register(proxy).build(); | ||
|
||
MultipleConstructors instance = child.newInstance(MultipleConstructors.class); | ||
|
||
assertSame(json, instance.json); | ||
assertSame(proxy, instance.proxy); | ||
} | ||
|
||
@Test | ||
public void shouldBeAbleToCallNonPublicConstructors() { | ||
Json json = new Json(); | ||
Injector injector = Injector.builder().register(json).build(); | ||
|
||
HiddenConstructor instance = injector.newInstance(HiddenConstructor.class); | ||
|
||
assertSame(json, instance.json); | ||
} | ||
|
||
public static class HiddenConstructor { | ||
|
||
private final Json json; | ||
|
||
HiddenConstructor(Json json) { | ||
this.json = json; | ||
} | ||
} | ||
|
||
} |