| /* |
| * 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.junit.scriptable; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.jcr.NodeIterator; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| import javax.jcr.observation.Event; |
| import javax.jcr.observation.EventIterator; |
| import javax.jcr.observation.EventListener; |
| import javax.jcr.query.Query; |
| |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceResolverFactory; |
| import org.apache.sling.engine.SlingRequestProcessor; |
| import org.apache.sling.jcr.api.SlingRepository; |
| import org.apache.sling.jcr.resource.api.JcrResourceConstants; |
| import org.apache.sling.junit.TestsProvider; |
| import org.osgi.framework.Constants; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.Reference; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** TestsProvider that provides test classes for repository |
| * nodes that have a sling:Test mixin. |
| */ |
| @Component(service = TestsProvider.class) |
| public class ScriptableTestsProvider implements TestsProvider { |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| private String pid; |
| private Session session; |
| private ResourceResolver resolver; |
| private long lastModified = System.currentTimeMillis(); |
| private long lastReloaded; |
| |
| /** List of resource paths that point to tests */ |
| private static List<String> testPaths = new LinkedList<>(); |
| |
| public static final String SLING_TEST_NODETYPE = "sling:Test"; |
| public static final String TEST_CLASS_NAME = ScriptableTestsProvider.class.getName(); |
| |
| /** Context that's passed to TestAllPaths */ |
| static class TestContext { |
| final List<String> testPaths; |
| final SlingRequestProcessor requestProcessor; |
| final ResourceResolver resourceResolver; |
| |
| TestContext(List<String> p, SlingRequestProcessor rp, ResourceResolver rr) { |
| testPaths = p; |
| requestProcessor = rp; |
| resourceResolver = rr; |
| } |
| } |
| |
| /** Need a ThreadLocal to pass context, as it's JUnit who instantiates the |
| * test classes, we can't easily decorate them (AFAIK). |
| */ |
| static final ThreadLocal<TestContext> testContext = new ThreadLocal<>(); |
| |
| /** We only consider test resources under the search path |
| * of the JCR resource resolver. These paths are supposed |
| * to be secured, as they contain other admin stuff anyway, |
| * so non-admin users are prevented from creating test nodes. |
| */ |
| private String[] allowedRoots; |
| |
| @Reference |
| private SlingRepository repository; |
| |
| @Reference |
| private SlingRequestProcessor requestProcessor; |
| |
| @Reference |
| private ResourceResolverFactory resolverFactory; |
| |
| // Need one listener per root path |
| private List<EventListener> listeners = new ArrayList<>(); |
| |
| class RootListener implements EventListener { |
| private final String path; |
| |
| RootListener(String path) { |
| this.path = path; |
| } |
| |
| @Override |
| public void onEvent(EventIterator it) { |
| log.debug("Change detected under {}, will reload list of test paths", path); |
| lastModified = System.currentTimeMillis(); |
| } |
| }; |
| |
| protected void activate(final Map<String, Object> props) throws Exception { |
| pid = (String)props.get(Constants.SERVICE_PID); |
| session = repository.loginAdministrative(repository.getDefaultWorkspace()); |
| Map<String, Object> auth = new HashMap<>(); |
| auth.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session); |
| resolver = resolverFactory.getResourceResolver(auth); |
| |
| // Copy resource resolver paths and make sure they end with a / |
| final String [] paths = resolver.getSearchPath(); |
| allowedRoots = new String[paths.length]; |
| System.arraycopy(paths, 0, allowedRoots, 0, paths.length); |
| for(int i=0; i < allowedRoots.length; i++) { |
| if(!allowedRoots[i].endsWith("/")) { |
| allowedRoots[i] += "/"; |
| } |
| } |
| |
| // Listen to changes to sling:Test nodes under allowed roots |
| final int eventTypes = |
| Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; |
| final boolean isDeep = true; |
| final boolean noLocal = true; |
| final String [] nodeTypes = { SLING_TEST_NODETYPE }; |
| final String [] uuid = null; |
| for(String path : allowedRoots) { |
| final EventListener listener = new RootListener(path); |
| listeners.add(listener); |
| session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, path, isDeep, uuid, nodeTypes, noLocal); |
| log.debug("Listening for JCR events under {}", path); |
| |
| } |
| |
| log.info("Activated, will look for test resources under {}", Arrays.asList(allowedRoots)); |
| } |
| |
| protected void deactivate() throws RepositoryException { |
| if (resolver != null) { |
| resolver.close(); |
| resolver = null; |
| } |
| if(session != null) { |
| for(EventListener listener : listeners) { |
| session.getWorkspace().getObservationManager().removeEventListener(listener); |
| } |
| listeners.clear(); |
| session.logout(); |
| } |
| session = null; |
| } |
| |
| @Override |
| public Class<?> createTestClass(String testName) throws ClassNotFoundException { |
| if(!testName.equals(TEST_CLASS_NAME)) { |
| throw new ClassNotFoundException(testName + " - the only valid name is " + TEST_CLASS_NAME); |
| } |
| |
| try { |
| maybeQueryTestResources(); |
| } catch(Exception e) { |
| throw new RuntimeException("Exception in maybeQueryTestResources()", e); |
| } |
| |
| if(testPaths.size() == 0) { |
| return ExplainTests.class; |
| } else { |
| testContext.set(new TestContext(testPaths, requestProcessor, resolver)); |
| return TestAllPaths.class; |
| } |
| } |
| |
| @Override |
| public String getServicePid() { |
| return pid; |
| } |
| |
| @Override |
| public List<String> getTestNames() { |
| // We have a single test to run, would be better to have one |
| // test class per test resource but that looks harder. Maybe |
| // use the Sling compiler to generate test classes? |
| final List<String> result = new LinkedList<>(); |
| result.add(TEST_CLASS_NAME); |
| return result; |
| } |
| |
| private List<String> maybeQueryTestResources() throws RepositoryException { |
| if(lastModified <= lastReloaded) { |
| log.debug("No changes detected, keeping existing list of {} test resources", testPaths.size()); |
| return testPaths; |
| } |
| |
| log.info("Changes detected, reloading list of test resources"); |
| final long reloadTime = System.currentTimeMillis(); |
| final List<String> newList = new LinkedList<>(); |
| |
| for(String root : allowedRoots) { |
| final String statement = "/jcr:root" + root + "/element(*, " + SLING_TEST_NODETYPE + ")"; |
| log.debug("Querying for test nodes: {}", statement); |
| session.refresh(true); |
| final Query q = session.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); |
| final NodeIterator it = q.execute().getNodes(); |
| while(it.hasNext()) { |
| final String path = it.nextNode().getPath(); |
| newList.add(path); |
| log.debug("Test resource found: {}", path); |
| } |
| } |
| log.info("List of test resources updated, {} resource(s) found under {}", |
| newList.size(), Arrays.asList(allowedRoots)); |
| |
| synchronized (testPaths) { |
| testPaths.clear(); |
| testPaths.addAll(newList); |
| } |
| |
| lastReloaded = reloadTime; |
| |
| return testPaths; |
| } |
| |
| @Override |
| public long lastModified() { |
| return lastModified; |
| } |
| |
| static TestContext getTestContext() { |
| return testContext.get(); |
| } |
| } |