diff --git a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java index 28af10cfc..e98d1a2d9 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -18,21 +18,12 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Throwables.throwIfUnchecked; -import static java.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Optional.ofNullable; -import static java.util.logging.Logger.getLogger; import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; import com.google.common.base.Supplier; import com.google.common.base.Throwables; -import com.google.common.io.CountingOutputStream; -import com.google.common.io.FileBackedOutputStream; - -import io.appium.java_client.internal.Config; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.Command; @@ -45,24 +36,13 @@ import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.ResponseCodec; import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; -import org.openqa.selenium.remote.http.Filter; import org.openqa.selenium.remote.http.HttpClient; -import org.openqa.selenium.remote.http.HttpHandler; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.HttpResponse; -import org.openqa.selenium.remote.http.WebSocket; -import org.openqa.selenium.remote.http.WebSocket.Listener; import org.openqa.selenium.remote.service.DriverService; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.UncheckedIOException; -import java.io.Writer; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.ConnectException; import java.net.URL; import java.util.Map; @@ -95,7 +75,6 @@ public AppiumCommandExecutor(Map additionalCommands, this(additionalCommands, null, checkNotNull(addressOfRemoteServer), httpClientFactory); } - public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer) { this(additionalCommands, addressOfRemoteServer, HttpClient.Factory.createDefault()); @@ -165,65 +144,13 @@ private Response createSession(Command command) throws IOException { if (getCommandCodec() != null) { throw new SessionNotCreatedException("Session already exists"); } - ProtocolHandshake handshake = new ProtocolHandshake() { - @SuppressWarnings("unchecked") - public Result createSession(HttpClient client, Command command) throws IOException { - Capabilities desiredCapabilities = (Capabilities) command.getParameters().get("desiredCapabilities"); - Capabilities desired = desiredCapabilities == null ? new ImmutableCapabilities() : desiredCapabilities; - - //the number of bytes before the stream should switch to buffering to a file - int threshold = (int) Math.min(Runtime.getRuntime().freeMemory() / 10, Integer.MAX_VALUE); - FileBackedOutputStream os = new FileBackedOutputStream(threshold); - try { - - CountingOutputStream counter = new CountingOutputStream(os); - Writer writer = new OutputStreamWriter(counter, UTF_8); - NewAppiumSessionPayload payload = NewAppiumSessionPayload.create(desired); - payload.writeTo(writer); - - try (InputStream rawIn = os.asByteSource().openBufferedStream(); - BufferedInputStream contentStream = new BufferedInputStream(rawIn)) { - - Method createSessionMethod = this.getClass().getSuperclass() - .getDeclaredMethod("createSession", HttpClient.class, InputStream.class, long.class); - createSessionMethod.setAccessible(true); - Optional result = (Optional) createSessionMethod.invoke(this, - client.with(httpHandler -> req -> { - req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); - return httpHandler.execute(req); - }), contentStream, counter.getCount()); - - return result.map(result1 -> { - Result toReturn = result.get(); - getLogger(ProtocolHandshake.class.getName()) - .info(format("Detected dialect: %s", toReturn.getDialect())); - return toReturn; - }).orElseThrow(() -> new SessionNotCreatedException( - format("Unable to create a new remote session. Desired capabilities = %s", desired))); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new SessionNotCreatedException(format("Unable to create a new remote session. " - + "Make sure your project dependencies config does not override " - + "Selenium API version %s used by java-client library.", - Config.main().getValue("selenium.version", String.class)), e); - } catch (InvocationTargetException e) { - String message = "Unable to create a new remote session."; - if (e.getCause() != null) { - if (e.getCause() instanceof WebDriverException) { - message += " Please check the server log for more details."; - } - message += format(" Original error: %s", e.getCause().getMessage()); - } - throw new SessionNotCreatedException(message, e); - } - } finally { - os.reset(); - } - } - }; - - ProtocolHandshake.Result result = handshake - .createSession(getClient(), command); + ProtocolHandshake.Result result = new ProtocolHandshake().createSession( + getClient().with((httpHandler) -> (req) -> { + req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); + return httpHandler.execute(req); + }), command + ); Dialect dialect = result.getDialect(); setCommandCodec(dialect.getCommandCodec()); getAdditionalCommands().forEach(this::defineCommand); @@ -241,9 +168,9 @@ public Response execute(Command command) throws WebDriverException { throw new WebDriverException(e.getMessage(), e); } }); - } - if (getAdditionalCommands().containsKey(command.getName())) { - super.defineCommand(command.getName(), getAdditionalCommands().get(command.getName())); + if (getAdditionalCommands().containsKey(command.getName())) { + super.defineCommand(command.getName(), getAdditionalCommands().get(command.getName())); + } } Response response; diff --git a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java b/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java index 1f4eb00f1..f1aba4b9c 100644 --- a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java +++ b/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java @@ -119,12 +119,6 @@ public interface MobileCapabilityType extends CapabilityType { */ String EVENT_TIMINGS = "eventTimings"; - /** - * This is the flag which forces server to switch to the mobile JSONWP. - * If {@code false} then it is switched to W3C mode. - */ - String FORCE_MJSONWP = "forceMjsonwp"; - /** * (Web and webview only) Enable ChromeDriver's (on Android) * or Safari's (on iOS) performance logging (default {@code false}). diff --git a/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java b/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java deleted file mode 100644 index 8d51f914d..000000000 --- a/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java +++ /dev/null @@ -1,539 +0,0 @@ -// 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.writeTo - -package io.appium.java_client.remote; - -import static com.google.common.collect.ImmutableMap.of; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; -import static io.appium.java_client.remote.MobileCapabilityType.FORCE_MJSONWP; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; -import static org.openqa.selenium.json.Json.LIST_OF_MAPS_TYPE; -import static org.openqa.selenium.json.Json.MAP_TYPE; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; -import com.google.common.io.CharSource; -import com.google.common.io.CharStreams; -import com.google.common.io.FileBackedOutputStream; - -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.json.Json; -import org.openqa.selenium.json.JsonInput; -import org.openqa.selenium.json.JsonOutput; -import org.openqa.selenium.remote.AcceptedW3CCapabilityKeys; -import org.openqa.selenium.remote.Dialect; -import org.openqa.selenium.remote.session.CapabilitiesFilter; -import org.openqa.selenium.remote.session.CapabilityTransform; -import org.openqa.selenium.remote.session.ChromeFilter; -import org.openqa.selenium.remote.session.EdgeFilter; -import org.openqa.selenium.remote.session.FirefoxFilter; -import org.openqa.selenium.remote.session.InternetExplorerFilter; -import org.openqa.selenium.remote.session.OperaFilter; -import org.openqa.selenium.remote.session.ProxyTransform; -import org.openqa.selenium.remote.session.SafariFilter; -import org.openqa.selenium.remote.session.StripAnyPlatform; -import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser; - -import java.io.Closeable; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringReader; -import java.io.Writer; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.TreeMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.annotation.Nullable; - -public class NewAppiumSessionPayload implements Closeable { - - private static final List APPIUM_CAPABILITIES = ImmutableList.builder() - .addAll(getAppiumCapabilities(MobileCapabilityType.class)) - .addAll(getAppiumCapabilities(AndroidMobileCapabilityType.class)) - .addAll(getAppiumCapabilities(IOSMobileCapabilityType.class)) - .addAll(getAppiumCapabilities(YouiEngineCapabilityType.class)).build(); - private static final String DESIRED_CAPABILITIES = "desiredCapabilities"; - private static final String CAPABILITIES = "capabilities"; - private static final String REQUIRED_CAPABILITIES = "requiredCapabilities"; - private static final String FIRST_MATCH = "firstMatch"; - private static final String ALWAYS_MATCH = "alwaysMatch"; - - private static final Predicate ACCEPTED_W3C_PATTERNS = new AcceptedW3CCapabilityKeys(); - - private final Set adapters; - private final Set transforms; - private final boolean forceMobileJSONWP; - - private final Json json = new Json(); - private final FileBackedOutputStream backingStore; - - private static List getAppiumCapabilities(Class capabilityList) { - return Arrays.stream(capabilityList.getDeclaredFields()).map(field -> { - field.setAccessible(true); - try { - return field.get(capabilityList).toString(); - } catch (IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - }).filter(s -> !FORCE_MJSONWP.equals(s)).collect(toList()); - } - - /** - * Creates instance of {@link NewAppiumSessionPayload}. - * - * @param caps capabilities to create a new session - * @return instance of {@link NewAppiumSessionPayload} - * @throws IOException On file system I/O error. - */ - public static NewAppiumSessionPayload create(Capabilities caps) throws IOException { - boolean forceMobileJSONWP = - ofNullable(caps.getCapability(FORCE_MJSONWP)) - .map(o -> Boolean.class.isAssignableFrom(o.getClass()) && Boolean.class.cast(o)) - .orElse(false); - - HashMap capabilityMap = new HashMap<>(caps.asMap()); - capabilityMap.remove(FORCE_MJSONWP); - Map source = of(DESIRED_CAPABILITIES, capabilityMap); - String json = new Json().toJson(source); - return new NewAppiumSessionPayload(new StringReader(json), forceMobileJSONWP); - } - - private NewAppiumSessionPayload(Reader source, boolean forceMobileJSONWP) throws IOException { - this.forceMobileJSONWP = forceMobileJSONWP; - // Dedicate up to 10% of all RAM or 20% of available RAM (whichever is smaller) to storing this - // payload. - int threshold = (int) Math.min( - Integer.MAX_VALUE, - Math.min( - Runtime.getRuntime().freeMemory() / 5, - Runtime.getRuntime().maxMemory() / 10)); - - backingStore = new FileBackedOutputStream(threshold); - try (Writer writer = new OutputStreamWriter(backingStore, UTF_8)) { - CharStreams.copy(source, writer); - } - - ImmutableSet.Builder adapters = ImmutableSet.builder(); - ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add); - adapters - .add(new ChromeFilter()) - .add(new EdgeFilter()) - .add(new FirefoxFilter()) - .add(new InternetExplorerFilter()) - .add(new OperaFilter()) - .add(new SafariFilter()); - this.adapters = adapters.build(); - - ImmutableSet.Builder transforms = ImmutableSet.builder(); - ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add); - transforms - .add(new ProxyTransform()) - .add(new StripAnyPlatform()) - .add(new W3CPlatformNameNormaliser()); - this.transforms = transforms.build(); - - ImmutableSet.Builder dialects = ImmutableSet.builder(); - if (getOss() != null) { - dialects.add(Dialect.OSS); - } - if (getAlwaysMatch() != null || getFirstMatch() != null) { - dialects.add(Dialect.W3C); - } - - validate(); - } - - private void validate() throws IOException { - Map alwaysMatch = getAlwaysMatch(); - if (alwaysMatch == null) { - alwaysMatch = of(); - } - Map always = alwaysMatch; - Collection> firsts = getFirstMatch(); - if (firsts == null) { - firsts = ImmutableList.of(of()); - } - - if (firsts.isEmpty()) { - throw new IllegalArgumentException("First match w3c capabilities is zero length"); - } - - //noinspection ResultOfMethodCallIgnored - firsts.stream() - .peek(map -> { - Set overlap = Sets.intersection(always.keySet(), map.keySet()); - if (!overlap.isEmpty()) { - throw new IllegalArgumentException( - "Overlapping keys between w3c always and first match capabilities: " + overlap); - } - }) - .map(first -> { - Map toReturn = new HashMap<>(); - toReturn.putAll(always); - toReturn.putAll(first); - return toReturn; - }) - .peek(map -> { - ImmutableSortedSet nullKeys = map.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(Map.Entry::getKey) - .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); - if (!nullKeys.isEmpty()) { - throw new IllegalArgumentException( - "Null values found in w3c capabilities. Keys are: " + nullKeys); - } - }) - .peek(map -> { - ImmutableSortedSet illegalKeys = map.entrySet().stream() - .filter(entry -> !ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .map(Map.Entry::getKey) - .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); - if (!illegalKeys.isEmpty()) { - throw new IllegalArgumentException( - "Illegal key values seen in w3c capabilities: " + illegalKeys); - } - }); - } - - /** - * Writes json capabilities to some appendable object. - * - * @param appendable to write a json - * @throws IOException On file system I/O error. - */ - public void writeTo(Appendable appendable) throws IOException { - try (JsonOutput json = new Json().newOutput(appendable)) { - json.beginObject(); - - Map first = getOss(); - if (first == null) { - //noinspection unchecked - first = (Map) stream().findFirst() - .orElse(new ImmutableCapabilities()) - .asMap(); - } - - // Write the first capability we get as the desired capability. - json.name(DESIRED_CAPABILITIES); - json.write(first); - - if (!forceMobileJSONWP) { - // And write the first capability for gecko13 - json.name(CAPABILITIES); - json.beginObject(); - - // Then write everything into the w3c payload. Because of the way we do this, it's easiest - // to just populate the "firstMatch" section. The spec says it's fine to omit the - // "alwaysMatch" field, so we do this. - json.name(FIRST_MATCH); - json.beginArray(); - getW3C().forEach(json::write); - json.endArray(); - - json.endObject(); // Close "capabilities" object - } - - writeMetaData(json); - - json.endObject(); - } - } - - private void writeMetaData(JsonOutput out) throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - switch (name) { - case CAPABILITIES: - case DESIRED_CAPABILITIES: - case REQUIRED_CAPABILITIES: - input.skipValue(); - break; - - default: - out.name(name); - out.write(input.read(Object.class)); - break; - } - } - } - } - - /** - * Stream the {@link Capabilities} encoded in the payload used to create this instance. The - * {@link Stream} will start with a {@link Capabilities} object matching the OSS capabilities, and - * will then expand each of the "{@code firstMatch}" and "{@code alwaysMatch}" contents as defined - * in the W3C WebDriver spec. The OSS {@link Capabilities} are listed first because converting the - * OSS capabilities to the equivalent W3C capabilities isn't particularly easy, so it's hoped that - * this approach gives us the most compatible implementation. - * - * @return The capabilities as a stream. - * @throws IOException On file system I/O error. - */ - public Stream stream() throws IOException { - // OSS first - Stream> oss = Stream.of(getOss()); - - // And now W3C - Stream> w3c = getW3C(); - - return Stream.concat(oss, w3c) - .filter(Objects::nonNull) - .map(this::applyTransforms) - .filter(Objects::nonNull) - .distinct() - .map(ImmutableCapabilities::new); - } - - @Override - public void close() throws IOException { - backingStore.reset(); - } - - private @Nullable Map getOss() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (DESIRED_CAPABILITIES.equals(name)) { - return input.read(MAP_TYPE); - } - input.skipValue(); - } - } - return null; - } - - private Stream> getW3C() throws IOException { - // If there's an OSS value, generate a stream of capabilities from that using the transforms, - // then add magic to generate each of the w3c capabilities. For the sake of simplicity, we're - // going to make the (probably wrong) assumption we can hold all of the firstMatch values and - // alwaysMatch value in memory at the same time. - Map oss = convertOssToW3C(getOss()); - Stream> fromOss; - - if (oss != null) { - Set usedKeys = new HashSet<>(); - - // Are there any values we care want to pull out into a mapping of their own? - List> firsts = adapters.stream() - .map(adapter -> adapter.apply(oss)) - .filter(Objects::nonNull) - .filter(map -> !map.isEmpty()) - .map(map -> - map.entrySet().stream() - .filter(entry -> entry.getKey() != null) - .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) - .peek(map -> usedKeys.addAll(map.keySet())) - .collect(ImmutableList.toImmutableList()); - if (firsts.isEmpty()) { - firsts = ImmutableList.of(of()); - } - - // Are there any remaining unused keys? - Map always = oss.entrySet().stream() - .filter(entry -> !usedKeys.contains(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); - - // Firsts contains at least one entry, always contains everything else. Let's combine them - // into the stream to form a unified set of capabilities. Woohoo! - fromOss = firsts.stream() - .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()) - .map(this::applyTransforms) - .map(map -> map.entrySet().stream() - .filter(entry -> !forceMobileJSONWP || ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .map((Function, Map.Entry>) stringObjectEntry -> - new Map.Entry() { - @Override - public String getKey() { - String key = stringObjectEntry.getKey(); - if (APPIUM_CAPABILITIES.contains(key) && !forceMobileJSONWP) { - return APPIUM_PREFIX + key; - } - return key; - } - - @Override - public Object getValue() { - return stringObjectEntry.getValue(); - } - - @Override - public Object setValue(Object value) { - return stringObjectEntry.setValue(value); - } - }) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))) - .map(map -> map); - } else { - fromOss = Stream.of(); - } - - Stream> fromW3c; - Map alwaysMatch = getAlwaysMatch(); - Collection> firsts = getFirstMatch(); - - if (alwaysMatch == null && firsts == null) { - fromW3c = Stream.of(); // No W3C capabilities. - } else { - if (alwaysMatch == null) { - alwaysMatch = of(); - } - Map always = alwaysMatch; // Keep the comoiler happy. - if (firsts == null) { - firsts = ImmutableList.of(of()); - } - - fromW3c = firsts.stream() - .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()); - } - - return Stream.concat(fromOss, fromW3c).distinct(); - } - - private @Nullable Map convertOssToW3C(Map capabilities) { - if (capabilities == null) { - return null; - } - - Map toReturn = new TreeMap<>(capabilities); - - // Platform name - if (capabilities.containsKey(PLATFORM) && !capabilities.containsKey(PLATFORM_NAME)) { - toReturn.put(PLATFORM_NAME, String.valueOf(capabilities.get(PLATFORM))); - } - - return toReturn; - } - - private @Nullable Map getAlwaysMatch() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (CAPABILITIES.equals(name)) { - input.beginObject(); - while (input.hasNext()) { - name = input.nextName(); - if (ALWAYS_MATCH.equals(name)) { - return input.read(MAP_TYPE); - } - input.skipValue(); - } - input.endObject(); - } else { - input.skipValue(); - } - } - } - return null; - } - - private @Nullable Collection> getFirstMatch() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (CAPABILITIES.equals(name)) { - input.beginObject(); - while (input.hasNext()) { - name = input.nextName(); - if (FIRST_MATCH.equals(name)) { - return input.read(LIST_OF_MAPS_TYPE); - } - input.skipValue(); - } - input.endObject(); - } else { - input.skipValue(); - } - } - } - return null; - } - - private Map applyTransforms(Map caps) { - Queue> toExamine = new LinkedList<>(caps.entrySet()); - Set seenKeys = new HashSet<>(); - Map toReturn = new TreeMap<>(); - - // Take each entry and apply the transforms - while (!toExamine.isEmpty()) { - Map.Entry entry = toExamine.remove(); - seenKeys.add(entry.getKey()); - - if (entry.getValue() == null) { - continue; - } - - for (CapabilityTransform transform : transforms) { - Collection> result = transform.apply(entry); - if (result == null) { - toReturn.remove(entry.getKey()); - break; - } - - for (Map.Entry newEntry : result) { - if (!seenKeys.contains(newEntry.getKey())) { - toExamine.add(newEntry); - continue; - } - if (newEntry.getKey().equals(entry.getKey())) { - entry = newEntry; - } - toReturn.put(newEntry.getKey(), newEntry.getValue()); - } - } - } - return toReturn; - } -}