org.openqa.selenium.grid.config.AnnotatedConfig Maven / Gradle / Ivy
Show all versions of selenium-grid Show documentation
// 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.grid.config;
import com.beust.jcommander.Parameter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.Primitives;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.openqa.selenium.internal.Require;
/**
* A form of {@link Config} that is generated by looking at fields in the constructor arg that are
* annotated with {@link ConfigValue}. The class hierarchy is walked from closest to Object to the
* constructor argument's type, null values are ignored, and the order in which fields are read is
* not stable (meaning duplicate config values may give different values each time).
*
* The main use of this class is to allow an object configured using (for example) jcommander to
* be used directly within the app, without requiring intermediate support classes to transform
* flags to config values.
*/
public class AnnotatedConfig implements Config {
private final Map>> config;
public AnnotatedConfig(Object obj) {
this(obj, Collections.emptySet(), false);
}
public AnnotatedConfig(Object obj, Set cliArgs, boolean includeCliArgs) {
Map>> values = new HashMap<>();
Deque allConfigValues = findConfigFields(obj.getClass());
for (Field field : allConfigValues) {
if (Map.class.isAssignableFrom(field.getType())) {
throw new ConfigException("Map fields may not be used for configuration: " + field);
}
field.setAccessible(true);
Object value;
try {
value = field.get(obj);
} catch (IllegalAccessException e) {
throw new ConfigException("Unable to read field: " + field);
}
ConfigValue annotation = field.getAnnotation(ConfigValue.class);
Parameter cliAnnotation = field.getAnnotation(Parameter.class);
boolean containsCliArg =
cliAnnotation != null && Arrays.stream(cliAnnotation.names()).anyMatch(cliArgs::contains);
if (cliArgs.size() > 0 && !containsCliArg && includeCliArgs) {
// Only getting config values for args entered by the user.
continue;
}
if (cliArgs.size() > 0 && containsCliArg && !includeCliArgs) {
// Excluding config values for args entered by the user.
continue;
}
Map> section =
values.computeIfAbsent(annotation.section(), str -> new HashMap<>());
List all = section.computeIfAbsent(annotation.name(), str -> new LinkedList<>());
if (value instanceof Collection) {
for (Object o : ((Collection>) value)) {
String singleValue = getSingleValue(o);
if (singleValue != null) {
all.add(singleValue);
}
}
} else {
String singleValue = getSingleValue(value);
if (singleValue != null) {
all.add(singleValue);
}
}
}
// Now make the config immutable.
this.config = values;
}
private String getSingleValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof Map) {
throw new ConfigException("Map fields may not be used for configuration: " + value);
}
if (value instanceof Collection) {
throw new ConfigException("Collection fields may not be used for configuration: " + value);
}
if (Boolean.FALSE.equals(value) && !Primitives.isWrapperType(value.getClass())) {
return null;
}
if (value instanceof Number && ((Number) value).floatValue() == 0f) {
return null;
}
return String.valueOf(value);
}
private Deque findConfigFields(Class> clazz) {
Deque toSet = new ArrayDeque<>();
Set> toVisit = new HashSet<>();
toVisit.add(clazz);
Set> seen = new HashSet<>();
while (!toVisit.isEmpty()) {
clazz = toVisit.iterator().next();
toVisit.remove(clazz);
seen.add(clazz);
Arrays.stream(clazz.getDeclaredFields())
.filter(field -> field.getAnnotation(ConfigValue.class) != null)
.forEach(toSet::addLast);
Class> toAdd = clazz.getSuperclass();
if (toAdd != null && !Object.class.equals(toAdd) && !seen.contains(toAdd)) {
toVisit.add(toAdd);
}
Arrays.stream(clazz.getInterfaces())
.filter(face -> !seen.contains(face))
.forEach(toVisit::add);
}
return toSet;
}
@Override
public Optional> getAll(String section, String option) {
Require.nonNull("Section name", section);
Require.nonNull("Option name", option);
Map> sec = config.get(section);
if (sec == null || sec.isEmpty()) {
return Optional.empty();
}
List values = sec.get(option);
if (values == null || values.isEmpty()) {
return Optional.empty();
}
return Optional.of(ImmutableList.copyOf(values));
}
@Override
public Set getSectionNames() {
return ImmutableSortedSet.copyOf(config.keySet());
}
@Override
public Set getOptions(String section) {
Require.nonNull("Section name to get options for", section);
return ImmutableSortedSet.copyOf(config.getOrDefault(section, ImmutableMap.of()).keySet());
}
}