Skip to content

Commit

Permalink
Add a lightweight Dependency Injection service
Browse files Browse the repository at this point in the history
  • Loading branch information
shs96c committed Jul 12, 2018
1 parent a062cd3 commit a6a19b1
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .buckconfig
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@
ie-test = //java/client/test/org/openqa/selenium/ie:ie
opera-test = //java/client/test/org/openqa/selenium/opera:opera
safari-test = //java/client/test/org/openqa/selenium/safari:safari
java-small-tests = //java/client/test/org/openqa/selenium:small-tests //java/client/test/org/openqa/selenium/json:small-tests //java/client/test/org/openqa/selenium/support:small-tests //java/client/test/org/openqa/selenium/remote:common-tests //java/client/test/org/openqa/selenium/remote:client-tests //java/server/test/org/openqa/grid/selenium/node:node //java/server/test/org/openqa/grid/selenium/proxy:proxy //java/server/test/org/openqa/selenium/remote/server:small-tests //java/server/test/org/openqa/selenium/remote/server/log:test
java-small-tests = //java/client/test/org/openqa/selenium:small-tests //java/client/test/org/openqa/selenium/json:small-tests //java/client/test/org/openqa/selenium/support:small-tests //java/client/test/org/openqa/selenium/remote:common-tests //java/client/test/org/openqa/selenium/remote:client-tests //java/server/test/org/openqa/grid/selenium/node:node //java/server/test/org/openqa/grid/selenium/proxy:proxy //java/server/test/org/openqa/selenium/remote/server:small-tests //java/server/test/org/openqa/selenium/remote/server/log:test //java/server/test/org/openqa/selenium/injector:small-tests
12 changes: 12 additions & 0 deletions java/server/src/org/openqa/selenium/injector/BUCK
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 java/server/src/org/openqa/selenium/injector/Injector.java
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);
}
}
}
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);
}
}
12 changes: 12 additions & 0 deletions java/server/test/org/openqa/selenium/injector/BUCK
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 java/server/test/org/openqa/selenium/injector/InjectorTest.java
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;
}
}

}

0 comments on commit a6a19b1

Please sign in to comment.