Added: release/sling/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,454 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * <p/> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p/> + * 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.apache.sling.testing.clients.indexing; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.sling.testing.clients.ClientException; +import org.apache.sling.testing.clients.SlingClient; +import org.apache.sling.testing.clients.SlingClientConfig; +import org.apache.sling.testing.clients.osgi.OsgiConsoleClient; +import org.apache.sling.testing.clients.query.QueryClient; +import org.apache.sling.testing.clients.util.poller.Polling; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.UUID.randomUUID; +import static org.apache.http.HttpStatus.SC_OK; + +/** + * <p>Interface to the oak indexing mechanism</p> + * + * <p>Exposes {@link #waitForAsyncIndexing(long, long)} for waiting all the indexing lanes to finish + * indexing and to guarantee all the indices are up to date</p> + * + * <p>For using {@link #waitForAsyncIndexing(long, long)}, the user must have access rights to:</p> + * <ul> + * <li>read/write in {@code /tmp}</li> + * <li>install bundles via {@link org.apache.sling.testing.clients.osgi.OsgiConsoleClient} + * (if the query servlet was not previously installed)</li> + * </ul> + * <p>In short, it requires administrative rights.</p> + */ +public class IndexingClient extends SlingClient { + private static final Logger LOG = LoggerFactory.getLogger(IndexingClient.class); + + /** Configuration name in {@link SlingClientConfig} to be used to initialize pre-defined index lanes + * Configured value, if any, is supposed to be an array of lane names + */ + private static final String INDEX_LANES_CSV_CONFIG_NAME = "indexLanesCsv"; + + /** Root of all the data created by this tool. Its presence marks that it was already installed */ + private static final String WAIT_FOR_ASYNC_INDEXING_ROOT = "/tmp/testing/waitForAsyncIndexing"; + + /** Where new index definitions are added */ + private static final String INDEX_PATH = WAIT_FOR_ASYNC_INDEXING_ROOT + "/oak:index"; + + /** Where the content to be indexed is created */ + private static final String CONTENT_PATH = WAIT_FOR_ASYNC_INDEXING_ROOT + "/content"; + + /** Prefix to be added to all the index names */ + private static final String INDEX_PREFIX = "testIndexingLane-"; + + /** Prefix to be added to all the properties */ + private static final String PROPERTY_PREFIX = "testProp-"; + + /** Prefix to be added to all the property values */ + private static final String VALUE_PREFIX = "testasyncval-"; + + /** Prefix to be added to all the tags */ + private static final String TAG_PREFIX = "testTag"; + + /** Placeholder for index name */ + private static final String INDEX_NAME_PLACEHOLDER = "<<INDEXNAME>>"; + + /** Placeholder for random, unique parts in content and queries */ + private static final String PROPERTY_PLACEHOLDER = "<<PROPNAME>>"; + + /** Placeholder for random, unique parts in content and queries */ + private static final String VALUE_PLACEHOLDER = "<<RANDVAL>>"; + + /** Placeholder for identifying the lane to which the index and the content belongs to */ + private static final String LANE_PLACEHOLDER = "<<LANE>>"; + + /** Placeholder for identifying the tag to which the index and the queries belongs to */ + private static final String TAG_PLACEHOLDER = "<<TAG>>"; + + /** Template for index definitions to be installed */ + private static final String INDEX_DEFINITION = "{" + + " '" + INDEX_NAME_PLACEHOLDER + "': {\n" + + " 'jcr:primaryType': 'oak:QueryIndexDefinition',\n" + + " 'type': 'lucene',\n" + + " 'async': '" + LANE_PLACEHOLDER + "',\n" + + " 'tags': '" + TAG_PLACEHOLDER + "',\n" + + " 'indexRules': {\n" + + " 'jcr:primaryType': 'nt:unstructured',\n" + + " 'nt:base': {\n" + + " 'jcr:primaryType': 'nt:unstructured',\n" + + " 'properties': {\n" + + " 'jcr:primaryType': 'nt:unstructured',\n" + + " '" + PROPERTY_PLACEHOLDER + "': {\n" + + " 'jcr:primaryType': 'nt:unstructured',\n" + + " 'name': '" + PROPERTY_PLACEHOLDER + "',\n" + + " 'analyzed': true\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }" + + "}"; + + /** Template for the content to be created and searched */ + private static final String CONTENT_DEFINITION = "{" + + "'testContent-" + LANE_PLACEHOLDER + "-" + VALUE_PLACEHOLDER + "': {" + + " 'jcr:primaryType': 'nt:unstructured', " + + " '" + PROPERTY_PLACEHOLDER +"': '" + VALUE_PLACEHOLDER + "'" + + "}}"; + + + /** Templates for queries to be executed against each index, in order of priority */ + private static final List<String> QUERIES = Arrays.asList( + // for Oak versions that support option(index tag testTag) + "/jcr:root" + WAIT_FOR_ASYNC_INDEXING_ROOT + "//*" + + "[jcr:contains(@" + PROPERTY_PLACEHOLDER + ", '" + VALUE_PLACEHOLDER +"')] " + + "option(traversal ok, index tag " + TAG_PLACEHOLDER + ")", + // for older Oak versions + "/jcr:root" + WAIT_FOR_ASYNC_INDEXING_ROOT + "//*" + + "[jcr:contains(@" + PROPERTY_PLACEHOLDER + ", '" + VALUE_PLACEHOLDER +"')] " + + "option(traversal ok)" + ); + + /** Global counter for how much time was spent in total waiting for async indexing */ + private static final AtomicLong totalWaited = new AtomicLong(); + public static final String ASYNC_INDEXER_CONFIG = "org.apache.jackrabbit.oak.plugins.index.AsyncIndexerService"; + + /** + * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b> + * + * @param http the underlying HttpClient to be used + * @param config sling specific configs + * @throws ClientException if the client could not be created + */ + public IndexingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException { + super(http, config); + } + + /** + * <p>Handy constructor easy to use in simple tests. Creates a client that uses basic authentication.</p> + * + * <p>For constructing clients with complex configurations, use a {@link InternalBuilder}</p> + * + * <p>For constructing clients with the same configuration, but a different class, use {@link #adaptTo(Class)}</p> + * + * @param url url of the server (including context path) + * @param user username for basic authentication + * @param password password for basic authentication + * @throws ClientException never, kept for uniformity with the other constructors + */ + public IndexingClient(URI url, String user, String password) throws ClientException { + super(url, user, password); + } + + /** + * Set provided {@code laneNames} to config map. This allows for subsequent initializations + * using {@code adaptTo} that shard the same config map to not require further configuration + * of lane names + * @param laneNames lane names to work on + */ + public void setLaneNames(String ... laneNames) { + getValues().put(INDEX_LANES_CSV_CONFIG_NAME, StringUtils.join(laneNames, ',')); + } + + /** + * Return the list of indexing lanes configured by {@link #setLaneNames}, if any. + * Else, retrieves configured lanes on the instance + * + * @return list of lane names + * @throws ClientException if the request fails + */ + public List<String> getLaneNames() throws ClientException { + List<String> configuredLanes = getConfiguredLaneNames(); + if (!configuredLanes.isEmpty()) { + return configuredLanes; + } + + Object configs = adaptTo(OsgiConsoleClient.class).getConfiguration(ASYNC_INDEXER_CONFIG).get("asyncConfigs"); + if (configs instanceof String[]) { + return Stream.of((String[]) configs).map(e -> e.split(":")[0]).collect(Collectors.toList()); + } else { + throw new ClientException("Cannot retrieve config from AsyncIndexerService, asyncConfigs is not a String[]"); + } + } + + private List<String> getConfiguredLaneNames() { + String configLanesCsv = getValue(INDEX_LANES_CSV_CONFIG_NAME); + if (configLanesCsv == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Stream.of(configLanesCsv.split(",")) + .map(e -> e.trim()).collect(Collectors.toList())); + } + + /** + * <p>Blocks until all the async indices are up to date, to guarantee that the susequent queries return + * all the results.</p> + * + * <p>Works by creating a custom index for each lane, adding specific content to + * be indexed by these indices and then repeatedly searching this content until everything is found (indexed). + * All the content is created under {@value #WAIT_FOR_ASYNC_INDEXING_ROOT}</p> + * + * <p>Indices are automatically created, but only if not already present. + * This method does not delete the indices at the end to avoid generating too much noise on the instance. + * To completely clean any traces, the user must call {@link #uninstall()}</p> + * + * <p>Requires administrative rights to install bundles and to create nodes under + * {@value #WAIT_FOR_ASYNC_INDEXING_ROOT}</p> + * + * @param timeout max time to wait, in milliseconds, before throwing {@code TimeoutException} + * @param delay time to sleep between retries + * @throws TimeoutException if the {@code timeout} was reached before all the indices were updated + * @throws InterruptedException to mark this method as waiting + * @throws ClientException if an error occurs during http requests/responses + */ + public void waitForAsyncIndexing(final long timeout, final long delay) + throws TimeoutException, InterruptedException, ClientException { + + install(); // will install only if needed + + final String uniqueValue = randomUUID().toString(); // to be added in all the content nodes + final List<String> lanes = getLaneNames(); // dynamically detect which lanes to wait for + + Polling p = new Polling(() -> searchContent(lanes, uniqueValue)); + + try { + createContent(lanes, uniqueValue); + p.poll(timeout, delay); + } finally { + long total = totalWaited.addAndGet(p.getWaited()); // count waited in all the cases (timeout) + LOG.info("Waited for async index {} ms (overall: {} ms)", p.getWaited(), total); + try { + deleteContent(uniqueValue); + } catch (ClientException e) { + LOG.warn("Failed to delete temporary content", e); + } + } + } + + /** + * Same as {@link #waitForAsyncIndexing(long timeout, long delay)}, + * but with default values for {@code timeout=1min} and {@code delay=500ms}. + * + * @see #waitForAsyncIndexing(long, long) + * + * @throws TimeoutException if the {@code timeout} was reached before all the indices were updated + * @throws InterruptedException to mark this method as waiting + * @throws ClientException if an error occurs during http requests/responses + */ + public void waitForAsyncIndexing() throws InterruptedException, ClientException, TimeoutException { + waitForAsyncIndexing(TimeUnit.MINUTES.toMillis(1), 500); + } + + /** + * <p>Creates the necessary custom indices in the repository, if not already present.</p> + * + * <p>It is automatically called in each wait, there's no need to + * explicitly invoke it from the test.</p> + * + * @throws ClientException if the installation fails + */ + public void install() throws ClientException { + if (exists(WAIT_FOR_ASYNC_INDEXING_ROOT)) { + LOG.debug("Skipping install since {} already exists", WAIT_FOR_ASYNC_INDEXING_ROOT); + return; + } + + createNodeRecursive(WAIT_FOR_ASYNC_INDEXING_ROOT, "sling:Folder"); + createNode(INDEX_PATH, "nt:unstructured"); + createNode(CONTENT_PATH, "sling:Folder"); + + final List<String> lanes = getLaneNames(); + for (String lane : lanes) { + String indexName = getIndexName(lane); + String indexDefinition = replacePlaceholders(INDEX_DEFINITION, lane, null); + LOG.info("Creating index {} in {}", indexName, INDEX_PATH); + LOG.debug("Index definition: {}", indexDefinition); + importContent(INDEX_PATH, "json", indexDefinition); + // Trigger reindex to make sure the complete index definition is used + setPropertyString(INDEX_PATH + "/" + indexName, "reindex", "true"); + } + } + + /** + * <p>Cleans all the data generated by {@link #install()} and {@link #waitForAsyncIndexing(long, long)}.</p> + * + * <p>User must manually call this if needed, as opposed to {@link #install()}, which is called + * automatically.</p> + * + * @deprecated Use #uninstallWithRetry + * @throws ClientException if the cleanup failed + */ + public void uninstall() throws ClientException { + this.deletePath(WAIT_FOR_ASYNC_INDEXING_ROOT, SC_OK); + } + + /** + * <p>Retries cleaning all the data generated by {@link #install()} and {@link #waitForAsyncIndexing(long, long)}.</p> + * + * <p>User must manually call this if needed, as opposed to {@link #install()}, which is called + * automatically.</p> + * + * @throws TimeoutException if retry operation times out and the path still exists + * @throws InterruptedException if the retry operation was interrupted by the user + */ + public void uninstallWithRetry() throws TimeoutException, InterruptedException { + new Polling(() -> { + this.deletePath(WAIT_FOR_ASYNC_INDEXING_ROOT, SC_OK); + return this.exists(WAIT_FOR_ASYNC_INDEXING_ROOT); + }).poll(5000, 500); + } + + /** + * Creates all the content structures to be indexed, one for each lane, + * with the given {@code uniqueValue}, to make them easily identifiable + * + * @param lanes list of lanes for which to create the content + * @param uniqueValue the unique value to be added + * @throws ClientException if the content creation fails + */ + private void createContent(final List<String> lanes, final String uniqueValue) throws ClientException { + // All the content is grouped under the same node + String contentHolder = CONTENT_PATH + "/" + uniqueValue; + LOG.debug("creating content in {}", contentHolder); + createNode(contentHolder, "sling:Folder"); + + for (String lane : lanes) { + String contentNode = replacePlaceholders(CONTENT_DEFINITION, lane, uniqueValue); + LOG.debug("creating: {}", contentNode); + importContent(contentHolder, "json", contentNode); + } + } + + /** + * Deletes the temporary nodes created in {@link #createContent(List, String)} + * + * @throws ClientException if the content cannot be deleted + */ + private void deleteContent(String uniqueValue) throws ClientException { + if (uniqueValue != null) { + String contentHolder = CONTENT_PATH + "/" + uniqueValue; + LOG.debug("deleting {}", contentHolder); + deletePath(contentHolder, SC_OK); + } + } + + /** + * Performs queries for each of the created content and checks that all return results + * + * @param lanes list of lanes for which to run queries + * @param uniqueValue the unique value to be used in queries + * @return true if all the queries returned at least one result (all indices are up to date) + * @throws ClientException if the http request failed + * @throws InterruptedException to mark this method as waiting + */ + private boolean searchContent(final List<String> lanes, final String uniqueValue) + throws ClientException, InterruptedException { + for (String lane : lanes) { + if (!searchContentForIndex(lane, uniqueValue)) { + return false; + } + } + // Queries returned at least one result for each index + return true; + } + + /** + * Tries all the known queries for a specific index lane, + * until one of them returns at least one result. + * + * @param lane the indexing lane to query + * @param uniqueValue the unique value to be used in queries + * @return true if at least one query returned results + * @throws ClientException if the http request fails + * @throws InterruptedException to mark this method as waiting + */ + private boolean searchContentForIndex(final String lane, final String uniqueValue) + throws ClientException, InterruptedException { + QueryClient queryClient = adaptTo(QueryClient.class); + + for (String query : QUERIES) { + // prepare the query with the final values + String indexName = getIndexName(lane); + String effectiveQuery = replacePlaceholders(query, lane, uniqueValue); + + try { + // Check query plan to make sure we use the good index + String plan = queryClient.getPlan(effectiveQuery, QueryClient.QueryType.XPATH); + if (plan.contains(indexName)) { + // The proper index is used, we can check the results + long results = queryClient.doCount(effectiveQuery, QueryClient.QueryType.XPATH); + if (results > 0) { + LOG.debug("Found {} results using query {}", results, effectiveQuery); + return true; + } + } else { + LOG.debug("Did not find index {} in plan: {}", indexName, plan); + LOG.debug("Will try the next query, if available"); + } + } catch (ClientException e) { + if (e.getHttpStatusCode() == 400) { + LOG.debug("Unsupported query: {}", effectiveQuery); + LOG.debug("Will try the next query, if available"); + } else { + // We don't continue if there's another problem + throw e; + } + } + } + // No query returned results + return false; + } + + private String replacePlaceholders(String original, String lane, String value) { + // Tags must be alphanumeric + String tag = StringUtils.capitalize(lane.replaceAll("[^A-Za-z0-9]", "")); + + String result = original; + result = StringUtils.replace(result, LANE_PLACEHOLDER, lane); + result = StringUtils.replace(result, INDEX_NAME_PLACEHOLDER, INDEX_PREFIX + lane); + result = StringUtils.replace(result, PROPERTY_PLACEHOLDER, PROPERTY_PREFIX + lane); + result = StringUtils.replace(result, VALUE_PLACEHOLDER, VALUE_PREFIX + value); + result = StringUtils.replace(result, TAG_PLACEHOLDER, TAG_PREFIX + tag); + + return result; + } + + private String getIndexName(final String lane) { + return INDEX_PREFIX + lane; + } +}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +@Version("0.2.0") +package org.apache.sling.testing.clients.indexing; + +import org.osgi.annotation.versioning.Version; Added: release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.instance; + +import java.net.URI; + +/** + * Configuration of a single instance instance. + */ +public class InstanceConfiguration { + + public static final String DEFAULT_ADMIN_USER = "admin"; + public static final String DEFAULT_ADMIN_PASSWORD = "admin"; + + private URI url; + private final String runmode; + private String adminUser; + private String adminPassword; + + public InstanceConfiguration(final URI url, final String runmode, String adminUser, String adminPassword) { + this.url = url; + this.runmode = runmode; + this.adminUser = adminUser; + this.adminPassword = adminPassword; + } + + public InstanceConfiguration(URI url, String runmode) { + this(url, runmode, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD); + } + + public URI getUrl() { + return this.url; + } + + public String getRunmode() { + return runmode; + } + + public String getAdminUser() { + return adminUser; + } + + public String getAdminPassword() { + return adminPassword; + } +} \ No newline at end of file Added: release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.instance; + +import org.apache.sling.testing.clients.SystemPropertiesConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for getting the current instance setup + */ +public final class InstanceSetup { + private static final Logger LOG = LoggerFactory.getLogger(InstanceSetup.class); + private static InstanceSetup SINGLETON; + + // TODO: JAVADOC + public static final String INSTANCE_CONFIG_INSTANCES = SystemPropertiesConfig.CONFIG_PROP_PREFIX + "instances"; + public static final String INSTANCE_CONFIG_URL = SystemPropertiesConfig.CONFIG_PROP_PREFIX + "instance.url."; + public static final String INSTANCE_CONFIG_RUNMODE = SystemPropertiesConfig.CONFIG_PROP_PREFIX + "instance.runmode."; + public static final String INSTANCE_CONFIG_ADMINUSER = SystemPropertiesConfig.CONFIG_PROP_PREFIX + "instance.adminUser."; + public static final String INSTANCE_CONFIG_ADMINPASSWORD = SystemPropertiesConfig.CONFIG_PROP_PREFIX + "instance.adminPassword."; + + /** + * @return the current setup object. + */ + public static InstanceSetup get() { + if ( SINGLETON == null ) { + SINGLETON = new InstanceSetup(); + } + return SINGLETON; + } + + private final List<InstanceConfiguration> configs = new ArrayList<InstanceConfiguration>(); + + private InstanceSetup() { + final int number = Integer.valueOf(System.getProperty(INSTANCE_CONFIG_INSTANCES, "0")); + for (int i=1; i<=number; i++ ) { + URI url; + try { + url = new URI(System.getProperty(INSTANCE_CONFIG_URL + String.valueOf(i))); + } catch (URISyntaxException e) { + LOG.error("Could not read URL for instance"); + continue; + } + final String runmode = System.getProperty(INSTANCE_CONFIG_RUNMODE + String.valueOf(i)); + final String adminUser = System.getProperty(INSTANCE_CONFIG_ADMINUSER + String.valueOf(i)); + final String adminPassword = System.getProperty(INSTANCE_CONFIG_ADMINPASSWORD + String.valueOf(i)); + + final InstanceConfiguration qc; + // Only pass in the admin user name and password if they're both set + if ((null == adminUser) || (null == adminPassword)) { + qc = new InstanceConfiguration(url, runmode); + } else { + qc = new InstanceConfiguration(url, runmode, adminUser, adminPassword); + } + + this.configs.add(qc); + } + } + + /** + * @return all instance configurations. + */ + public List<InstanceConfiguration> getConfigurations() { + return this.configs; + } + + /** + * Get the list of all InstanceConfiguration with a specific {@code runmode} + * + * @param runmode the desired runmode + * @return all instance configurations filtered by runmode. + */ + public List<InstanceConfiguration> getConfigurations(final String runmode) { + final List<InstanceConfiguration> result = new ArrayList<InstanceConfiguration>(); + for(final InstanceConfiguration qc : this.configs) { + if ( runmode == null || runmode.equals(qc.getRunmode()) ) { + result.add(qc); + } + } + return result; + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/instance/package-info.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/instance/package-info.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/instance/package-info.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +@Version("1.1.0") +package org.apache.sling.testing.clients.instance; + +import org.osgi.annotation.versioning.Version; Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.interceptors; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.io.InterruptedIOException; + +public class DelayRequestInterceptor implements HttpRequestInterceptor { + + private final long milliseconds; + + public DelayRequestInterceptor(long milliseconds) { + this.milliseconds = milliseconds; + } + + public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { + if (milliseconds <= 0) { + return; + } + + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/FormBasedAuthInterceptor.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/FormBasedAuthInterceptor.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/FormBasedAuthInterceptor.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.interceptors; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthScope; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HttpContext; +import org.apache.sling.testing.clients.util.ServerErrorRetryStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; + +public class FormBasedAuthInterceptor implements HttpRequestInterceptor { + static final Logger LOG = LoggerFactory.getLogger(FormBasedAuthInterceptor.class); + + private final String loginPath = "j_security_check"; + private final String loginTokenName; + + public FormBasedAuthInterceptor(String loginTokenName) { + this.loginTokenName = loginTokenName; + } + + public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { + final URI uri = URI.create(request.getRequestLine().getUri()); + if (uri.getPath().endsWith(loginPath)) { + LOG.debug("Request ends with {} so I'm not intercepting the request", loginPath); + return; + } + + Cookie loginCookie = getLoginCookie(context, loginTokenName); + if (loginCookie != null) { + LOG.debug("Request has cookie {}={} so I'm not intercepting the request", + loginCookie.getName(), loginCookie.getValue()); + return; + } + + // get host + final HttpHost host = HttpClientContext.adapt(context).getTargetHost(); + + // get the username and password from the credentials provider + final CredentialsProvider credsProvider = HttpClientContext.adapt(context).getCredentialsProvider(); + final AuthScope scope = new AuthScope(host.getHostName(), host.getPort()); + final String username = credsProvider.getCredentials(scope).getUserPrincipal().getName(); + final String password = credsProvider.getCredentials(scope).getPassword(); + + List<NameValuePair> parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair("j_username", username)); + parameters.add(new BasicNameValuePair("j_password", password)); + HttpEntity httpEntity = new UrlEncodedFormEntity(parameters, "utf-8"); + + HttpPost loginPost = new HttpPost(URI.create(request.getRequestLine().getUri()).resolve(loginPath)); + loginPost.setEntity(httpEntity); + + final CloseableHttpClient client = HttpClientBuilder.create() + .setServiceUnavailableRetryStrategy(new ServerErrorRetryStrategy()) + .disableRedirectHandling() + .build(); + + client.execute(host, loginPost, context); + + } + + /** Get login token cookie or null if not found */ + private Cookie getLoginCookie(HttpContext context, String loginTokenName) { + for (Cookie cookie : HttpClientContext.adapt(context).getCookieStore().getCookies()) { + if (cookie.getName().equalsIgnoreCase(loginTokenName) && !cookie.getValue().isEmpty()) { + return cookie; + } + } + return null; + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,40 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * <p/> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p/> + * 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.apache.sling.testing.clients.interceptors; + +import org.apache.http.cookie.Cookie; +import org.apache.sling.testing.clients.SystemPropertiesConfig; + +public class StickyCookieHolder { + + private static final ThreadLocal<Cookie> testStickySessionCookie = new ThreadLocal<Cookie>(); + public static final String COOKIE_NAME = System.getProperty(SystemPropertiesConfig.CONFIG_PROP_PREFIX + "session.cookie.name", "test_session_id"); + + public static Cookie getTestStickySessionCookie() { + return testStickySessionCookie.get(); + } + + public static void setTestStickySessionCookie(Cookie stickySessionCookie) { + testStickySessionCookie.set(stickySessionCookie); + } + + public static void remove() { + testStickySessionCookie.remove(); + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.interceptors; + + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.util.List; +import java.util.ListIterator; + +public class StickyCookieInterceptor implements HttpRequestInterceptor { + + public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { + final HttpClientContext clientContext = HttpClientContext.adapt(httpContext); + List<Cookie> cookies = clientContext.getCookieStore().getCookies(); + boolean set = (null != StickyCookieHolder.getTestStickySessionCookie()); + boolean found = false; + ListIterator<Cookie> it = cookies.listIterator(); + while (it.hasNext()) { + Cookie cookie = it.next(); + if (cookie.getName().equals(StickyCookieHolder.COOKIE_NAME)) { + found = true; + if (set) { + // set the cookie with the value saved for each thread using the rule + it.set(StickyCookieHolder.getTestStickySessionCookie()); + } else { + // if the cookie is not set in TestStickySessionRule, remove it from here + it.remove(); + } + } + } + // if the cookie needs to be set from TestStickySessionRule but did not exist in the client cookie list, add it here. + if (!found && set) { + cookies.add(StickyCookieHolder.getTestStickySessionCookie()); + } + BasicCookieStore cs = new BasicCookieStore(); + cs.addCookies(cookies.toArray(new Cookie[cookies.size()])); + httpContext.setAttribute(HttpClientContext.COOKIE_STORE, cs); + } +} \ No newline at end of file Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.interceptors; + +import org.apache.http.Header; +import org.apache.http.cookie.Cookie; +import org.apache.http.cookie.CookieOrigin; +import org.apache.http.cookie.CookiePathComparator; +import org.apache.http.cookie.MalformedCookieException; +import org.apache.http.impl.cookie.DefaultCookieSpec; + +import java.util.List; + +public class StickyCookieSpec extends DefaultCookieSpec { + private final static CookiePathComparator PATH_COMPARATOR = new CookiePathComparator(); + + @Override + public List<Cookie> parse(Header header, CookieOrigin origin) throws MalformedCookieException { + List<Cookie> cookies = super.parse(header, origin); + for (Cookie cookie : cookies) { + if (cookie.getName().equals(StickyCookieHolder.COOKIE_NAME)) { + // store it in the TestStickySessionRule threadlocal var + StickyCookieHolder.setTestStickySessionCookie(cookie); + } + } + return cookies; + } + + @Override + public List<Header> formatCookies(List<Cookie> cookies) { + return super.formatCookies(cookies); + } +} \ No newline at end of file Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,49 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * <p/> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p/> + * 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.apache.sling.testing.clients.interceptors; + +public class TestDescriptionHolder { + + private static final ThreadLocal<String> methodName = new ThreadLocal<String>(); + private static final ThreadLocal<String> className = new ThreadLocal<String>(); + + public static String getMethodName() { + return methodName.get(); + } + + public static void setMethodName(String methodName) { + TestDescriptionHolder.methodName.set(methodName); + } + + public static void removeMethodName() { + TestDescriptionHolder.methodName.remove(); + } + + public static String getClassName() { + return className.get(); + } + + public static void setClassName(String className) { + TestDescriptionHolder.className.set(className); + } + + public static void removeClassName() { + TestDescriptionHolder.className.remove(); + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.sling.testing.clients.interceptors; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; + +/** + * HttpClient interceptor that propagates the current test name as part HTTP request headers. + * Headers can then be logged, exported as MDC info etc. by {@code TestNameLoggingFilter}. + * + * Meant to help in correlating the server side logs with the test case being executed. + * + * @see org.slf4j.MDC http://www.slf4j.org/manual.html + */ +public class TestDescriptionInterceptor implements HttpRequestInterceptor{ + //Same headers are defined in TestLogServlet + public static final String TEST_CLASS_HEADER = "X-Sling-TestClass"; + public static final String TEST_NAME_HEADER = "X-Sling-TestName"; + + public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { + addHeader(httpRequest, TEST_NAME_HEADER, TestDescriptionHolder.getMethodName()); + addHeader(httpRequest, TEST_CLASS_HEADER, TestDescriptionHolder.getClassName()); + } + + private static void addHeader(HttpRequest httpRequest, String name, String value){ + if (value != null) { + httpRequest.addHeader(name, value); + } + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +@Version("1.1.0") +package org.apache.sling.testing.clients.interceptors; + +import org.osgi.annotation.versioning.Version; Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +/** + * Thin Wrapper around a Bundle definition JSON + */ +public class Bundle { + + public enum Status { + + ACTIVE("Active"), + + FRAGMENT("Fragment"), + + RESOLVED("Resolved"), + + INSTALLED("Installed"); + + String value; + + Status(String value) { + this.value = value; + } + + public static Status value(String o) { + for(Status s : values()) { + if(s.value.equalsIgnoreCase(o)) { + return s; + } + } + return null; + } + + public String toString() { + return value; + } + } + +} \ No newline at end of file Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +import org.apache.sling.testing.clients.ClientException; +import org.codehaus.jackson.JsonNode; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class BundleInfo { + + private JsonNode bundle; + + public BundleInfo(JsonNode root) throws ClientException { + if(root.get("id") != null) { + if(root.get("id") == null) { + throw new ClientException("No Bundle Info returned"); + } + bundle = root; + } else { + if(root.get("data") == null && root.get("data").size() < 1) { + throw new ClientException("No Bundle Info returned"); + } + bundle = root.get("data").get(0); + } + } + + /** + * @return the bundle identifier + */ + public int getId() { + return bundle.get("id").getIntValue(); + } + + /** + * @return the bundle name + */ + public String getName() { + return bundle.get("name").getTextValue(); + } + + /** + * @return the bundle version + */ + public String getVersion() { + return bundle.get("version").getTextValue(); + } + + /** + * Returns the indicator if the bundle is a fragment + * + * @return {@code true} if bundle is a fragment, {@code false} otherwise. + */ + public boolean isFragment() { + return bundle.get("fragment").getBooleanValue(); + } + + /** + * @return the bundle current state + */ + public Bundle.Status getStatus() { + return Bundle.Status.value(bundle.get("state").getTextValue()); + } + + /** + * @return the bundle symbolic name + */ + public String getSymbolicName() { + return bundle.get("symbolicName").getTextValue(); + } + + /** + * @return the category of the bundle + */ + public String getCategory() { + return bundle.get("category").getTextValue(); + } + + /** + * Returns the value of a specific key in the bundle + * + * @param key the property to search + * @return a specific bundle property + */ + public String getProperty(String key) { + Map<String, String> props = getProperties(); + return props.get(key); + } + + /** + * @return the bundle properties in a {@link Map} + */ + public Map<String, String> getProperties() { + JsonNode props = bundle.get("props"); + Map<String, String> entries = new HashMap<String, String>(); + + if(props != null) { + Iterator<JsonNode> it = props.getElements(); + while(it.hasNext()) { + JsonNode n = it.next(); + entries.put(n.get("key").getTextValue(), n.get("value").getTextValue()); + } + } + return entries; + } + +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +import org.apache.sling.testing.clients.ClientException; +import org.codehaus.jackson.JsonNode; + +import java.util.Iterator; + +/** + * A simple Wrapper around the returned JSON when requesting the status of /system/console/bundles + */ +public class BundlesInfo { + + private JsonNode root = null; + + private JsonNode status = null; + + /** + * The only constructor. + * + * @param root the root JSON node of the bundles info. + * @throws ClientException if the json does not contain the proper info + */ + public BundlesInfo(JsonNode root) throws ClientException { + this.root = root; + // some simple sanity checks + if(root.get("s") == null) + throw new ClientException("No Status Info returned!"); + if(root.get("s").size() != 5) + throw new ClientException("Wrong number of status numbers listed!"); + status = root.get("s"); + } + + /** + * @return the status message of the bundle context + * @throws ClientException if the request cannot be completed + */ + public String getStatusMessage() throws ClientException { + if(root.get("status") == null) + throw new ClientException("No Status message returned!"); + return root.get("status").getValueAsText(); + } + + /** + * @return total number of bundles. + */ + public int getTotalNumOfBundles() { + return Integer.parseInt(status.get(0).getValueAsText()); + } + + /** + * Returns number of bundles that are in specified state + * + * @param status the requested status + * @return the number of bundles + */ + public int getNumBundlesByStatus(Bundle.Status status) { + int index = -1; + switch(status) { + case ACTIVE: + index = 1; + break; + case FRAGMENT: + index = 2; + break; + case RESOLVED: + index = 3; + break; + case INSTALLED: + index = 4; + break; + } + return Integer.parseInt(this.status.get(index).getValueAsText()); + } + + /** + * Return bundle info for a bundle with persistence identifier {@code pid} + * + * @param id the id of the bundle + * @return the BundleInfo + * @throws ClientException if the info could not be retrieved + */ + public BundleInfo forId(String id) throws ClientException { + JsonNode bundle = findBy("id", id); + return (bundle != null) ? new BundleInfo(bundle) : null; + } + + /** + * Return bundle info for a bundle with name {@code name} + * + * @param name the name of the requested bundle + * @return the info, or {@code null} if the bundle is not found + * @throws ClientException if the info cannot be retrieved + */ + public BundleInfo forName(String name) throws ClientException { + JsonNode bundle = findBy("name", name); + return (bundle != null) ? new BundleInfo(bundle) : null; + } + + /** + * Return bundle info for a bundle with symbolic name {@code name} + * + * @param name the symbolic name of the requested bundle + * @return the info, or {@code null} if the bundle is not found + * @throws ClientException if the info cannot be retrieved + */ + public BundleInfo forSymbolicName(String name) throws ClientException { + JsonNode bundle = findBy("symbolicName", name); + return (bundle != null) ? new BundleInfo(bundle) : null; + } + + private JsonNode findBy(String key, String value) { + Iterator<JsonNode> nodes = root.get("data").getElements(); + while(nodes.hasNext()) { + JsonNode node = nodes.next(); + if ((null != node.get(key)) && (node.get(key).isValueNode())) { + final String valueNode = node.get(key).getTextValue(); + if (valueNode.equals(value)) { + return node; + } + } + } + return null; + } + +} \ No newline at end of file Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.sling.testing.clients.osgi; + +import org.apache.sling.testing.clients.ClientException; +import org.apache.sling.testing.clients.util.poller.Polling; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeoutException; + + +/** + * Utility for installing and starting additional bundles for testing + */ +public class BundlesInstaller { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final OsgiConsoleClient osgiConsoleClient; + public static final String ACTIVE_STATE = "active"; + + public BundlesInstaller(OsgiConsoleClient cc) { + osgiConsoleClient = cc; + } + + /** + * Checks if a bundle is installed or not. Does not retry. + * @param bundleFile bundle file + * @return true if the bundle is installed + * @throws ClientException if the state of the bundle could not be determined + */ + public boolean isInstalled(File bundleFile) throws ClientException { + String bundleSymbolicName = ""; + try { + bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(bundleFile); + log.debug("Checking if installed: " + bundleSymbolicName); + + osgiConsoleClient.getBundleState(bundleSymbolicName); + log.debug("Already installed: " + bundleSymbolicName); + return true; + } catch (ClientException e) { + log.debug("Not yet installed: " + bundleSymbolicName); + return false; + } catch (IOException e) { + log.debug("Failed to retrieve bundle symbolic name from file. ", e); + throw new ClientException("Failed to retrieve bundle symbolic name from file. ", e); + } + } + + /** + * Check if the installed version matches the one of the bundle (file) + * @param bundleFile bundle file + * @return true if the bundle is installed and has the same version + * @throws ClientException if the installed version cannot be retrieved + * @throws IOException if the file version cannot be read + */ + public boolean isInstalledWithSameVersion(File bundleFile) throws ClientException, IOException { + final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(bundleFile); + final String versionOnServer = osgiConsoleClient.getBundleVersion(bundleSymbolicName); + final String versionInBundle = OsgiConsoleClient.getBundleVersionFromFile(bundleFile); + if (versionOnServer.equals(versionInBundle)) { + return true; + } else { + log.warn("Installed bundle doesn't match: {}, versionOnServer={}, versionInBundle={}", + bundleSymbolicName, versionOnServer, versionInBundle); + return false; + } + } + + /** + * Install a list of bundles supplied as Files + * @param toInstall list ob bundles to install + * @param startBundles whether to start the bundles + * @throws ClientException if an error occurs during installation + * @throws IOException if reading the file fails + */ + public void installBundles(List<File> toInstall, boolean startBundles) throws ClientException, IOException { + for(File f : toInstall) { + final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(f); + if (isInstalled(f)) { + if (f.getName().contains("SNAPSHOT")) { + log.info("Reinstalling (due to SNAPSHOT version): {}", bundleSymbolicName); + osgiConsoleClient.uninstallBundle(bundleSymbolicName); + } else if (!isInstalledWithSameVersion(f)) { + log.info("Reinstalling (due to version mismatch): {}", bundleSymbolicName); + osgiConsoleClient.uninstallBundle(bundleSymbolicName); + } else { + log.info("Not reinstalling: {}", bundleSymbolicName); + continue; + } + } + osgiConsoleClient.installBundle(f, startBundles); + log.info("Installed: {}", bundleSymbolicName); + } + + // ensure that bundles are re-wired esp. if an existing bundle was updated + osgiConsoleClient.refreshPackages(); + + log.info("{} additional bundles installed", toInstall.size()); + } + + /** + * Uninstall a list of bundles supplied as Files + * @param toUninstall bundles to uninstall + * @throws ClientException if one of the requests failed + * @throws IOException if the files cannot be read from disk + */ + public void uninstallBundles(List<File> toUninstall) throws ClientException, IOException { + for(File f : toUninstall) { + final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(f); + if (isInstalled(f)) { + log.info("Uninstalling bundle: {}", bundleSymbolicName); + osgiConsoleClient.uninstallBundle(bundleSymbolicName); + } else { + log.info("Could not uninstall: {} as it never was installed", bundleSymbolicName); + } + } + + // ensure that bundles are re-wired esp. if an existing bundle was updated + osgiConsoleClient.refreshPackages(); + + log.info("{} additional bundles uninstalled", toUninstall.size()); + } + + + /** + * Wait for all bundles specified in symbolicNames list to be installed in the OSGi web console. + * @deprecated use {@link #waitBundlesInstalled(List, long)} + * @param symbolicNames the list of names for the bundles + * @param timeoutSeconds how many seconds to wait + * @throws ClientException if something went wrong + * @throws InterruptedException if interrupted + * @return true if all the bundles were installed + */ + @Deprecated + public boolean waitForBundlesInstalled(List<String> symbolicNames, int timeoutSeconds) throws ClientException, InterruptedException { + log.info("Checking that the following bundles are installed (timeout {} seconds): {}", timeoutSeconds, symbolicNames); + for (String symbolicName : symbolicNames) { + boolean started = osgiConsoleClient.checkBundleInstalled(symbolicName, 500, 2 * timeoutSeconds); + if (!started) return false; + } + return true; + } + + /** + * Wait for multiple bundles to be installed in the OSGi web console. + * @param symbolicNames the list bundles to be checked + * @param timeout max total time to wait for all bundles, in ms, before throwing {@code TimeoutException} + * @throws TimeoutException if the timeout was reached before all the bundles were installed + * @throws InterruptedException to mark this operation as "waiting", callers should rethrow it + */ + public void waitBundlesInstalled(List<String> symbolicNames, long timeout) + throws InterruptedException, TimeoutException { + log.info("Checking that the following bundles are installed (timeout {} ms): {}", timeout, symbolicNames); + long start = System.currentTimeMillis(); + for (String symbolicName : symbolicNames) { + osgiConsoleClient.waitBundleInstalled(symbolicName, timeout, 500); + + if (System.currentTimeMillis() > start + timeout) { + throw new TimeoutException("Waiting for bundles did not finish in " + timeout + " ms."); + } + } + } + + /** + * Start all the bundles in a {{List}} + * @param symbolicNames the list of bundles to start + * @param timeout total max time to wait for all the bundles, in ms + * @throws TimeoutException if the timeout is reached before all the bundles are started + * @throws InterruptedException to mark this operation as "waiting", callers should rethrow it + */ + public void startAllBundles(final List<String> symbolicNames, int timeout) throws InterruptedException, TimeoutException { + log.info("Starting bundles (timeout {} seconds): {}", timeout, symbolicNames); + + Polling p = new Polling() { + @Override + public Boolean call() throws Exception { + boolean allActive = true; + for (String bundle : symbolicNames) { + String state = osgiConsoleClient.getBundleState(bundle); + if (!state.equalsIgnoreCase(ACTIVE_STATE)) { + osgiConsoleClient.startBundle(bundle); + allActive = false; + } + } + return allActive; + } + }; + + p.poll(timeout, 500); + } +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Component.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Component.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/Component.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +public class Component { + + public enum Status { + + // the states being used in the DS Felix WebConsole are listed in https://github.com/apache/felix/blob/6e5cde8471febb36bc72adeba85989edba943188/webconsole-plugins/ds/src/main/java/org/apache/felix/webconsole/plugins/ds/internal/ComponentConfigurationPrinter.java#L374 + ACTIVE("active"), + + SATISFIED("satisfied"), + + UNSATISFIED_CONFIGURATION("unsatisfied (configuration)"), + + UNSATISFIED_REFERENCE("unsatisfied (reference)"), + + FAILED_ACTIVATION("failed activation"), + + UNKNOWN("unknown"); + + String value; + + Status(String value) { + this.value = value; + } + + public static Status value(String o) { + for(Status s : values()) { + if(s.value.equalsIgnoreCase(o)) { + return s; + } + } + return UNKNOWN; + } + + public String toString() { + return value; + } + + } + +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +import org.apache.sling.testing.clients.ClientException; +import org.codehaus.jackson.JsonNode; + +public class ComponentInfo { + + private JsonNode component; + + public ComponentInfo(JsonNode root) throws ClientException { + if(root.get("id") != null) { + if(root.get("id") == null) { + throw new ClientException("No Component Info returned"); + } + component = root; + } else { + if(root.get("data") == null && root.get("data").size() < 1) { + throw new ClientException("No Component Info returned"); + } + component = root.get("data").get(0); + } + } + + /** + * @return the component identifier + */ + public int getId() { + return component.get("id").getIntValue(); + } + + /** + * @return the component name + */ + public String getName() { + return component.get("name").getTextValue(); + } + + /** + * @return the component status + */ + public Component.Status getStatus() { + return Component.Status.value(component.get("state").getTextValue()); + } + + /** + * @return the component persistent identifier + */ + public String getPid() { + return component.get("pid").getTextValue(); + } + +} Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java ============================================================================== --- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java (added) +++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java Thu Apr 9 13:50:42 2020 @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * 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.apache.sling.testing.clients.osgi; + +import org.apache.sling.testing.clients.ClientException; +import org.codehaus.jackson.JsonNode; + +import java.util.Iterator; + +/** + * Thin wrapper around the list of components + */ +public class ComponentsInfo { + + private JsonNode root = null; + + /** + * The only constructor. + * + * @param rootNode the root JSON node of the components info. + * @throws ClientException if the info cannot be retrieved + */ + public ComponentsInfo(JsonNode rootNode) throws ClientException { + this.root = rootNode; + } + + /** + * @return the number of installed components + * @throws ClientException if the info cannot be retrieved + */ + public int getNumberOfInstalledComponents() throws ClientException { + if(root.get("status") == null) + throw new ClientException("Number of installed Components not defined!"); + return Integer.parseInt(root.get("status").getValueAsText()); + } + + /** + * @param id the id of the component + * @return the ComponentInfo for a component with the identifier {@code id} + * @throws ClientException if the info cannot be retrieved + */ + public ComponentInfo forId(String id) throws ClientException { + JsonNode component = findBy("id", id); + return (component != null) ? new ComponentInfo(component) : null; + } + + /** + * @param name the name of the component + * @return the ComponentInfo for a component with the name {@code name} + * @throws ClientException if the info cannot be retrieved + */ + public ComponentInfo forName(String name) throws ClientException { + JsonNode component = findBy("name", name); + return (component != null) ? new ComponentInfo(component) : null; + } + + /** + * @param pid the pid of the component + * @return the ComponentInfo for a component with the pid {@code pid} + * @throws ClientException if the info cannot be retrieved + */ + public ComponentInfo forPid(String pid) throws ClientException { + JsonNode component = findBy("pid", pid); + return (component != null) ? new ComponentInfo(component) : null; + } + + private JsonNode findBy(String key, String value) { + Iterator<JsonNode> nodes = root.get("data").getElements(); + while(nodes.hasNext()) { + JsonNode node = nodes.next(); + if ((null != node.get(key)) && (node.get(key).isValueNode())) { + final String valueNode = node.get(key).getTextValue(); + if (valueNode.equals(value)) { + return node; + } + } + } + return null; + } +} \ No newline at end of file
