View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2019 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.handler.url.impl.clientlib;
21  
22  import static javax.jcr.observation.Event.NODE_ADDED;
23  import static javax.jcr.observation.Event.NODE_MOVED;
24  import static javax.jcr.observation.Event.NODE_REMOVED;
25  import static javax.jcr.observation.Event.PROPERTY_ADDED;
26  import static javax.jcr.observation.Event.PROPERTY_CHANGED;
27  import static javax.jcr.observation.Event.PROPERTY_REMOVED;
28  
29  import java.util.Collections;
30  
31  import javax.jcr.Node;
32  import javax.jcr.RepositoryException;
33  import javax.jcr.Session;
34  import javax.jcr.observation.EventIterator;
35  import javax.jcr.observation.EventListener;
36  
37  import org.apache.jackrabbit.api.observation.JackrabbitEventFilter;
38  import org.apache.jackrabbit.api.observation.JackrabbitObservationManager;
39  import org.apache.sling.api.resource.LoginException;
40  import org.apache.sling.api.resource.Resource;
41  import org.apache.sling.api.resource.ResourceResolver;
42  import org.apache.sling.api.resource.ResourceResolverFactory;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import com.github.benmanes.caffeine.cache.Caffeine;
47  import com.github.benmanes.caffeine.cache.LoadingCache;
48  
49  /**
50   * Checks given path for client library folder and allowProxy flag status.
51   * Caches the results in memory.
52   * Clears cache if any observation events occured on client libraries.
53   */
54  final class ClientlibPathCache implements EventListener, AutoCloseable {
55  
56    private final ResourceResolverFactory resourceResolverFactory;
57    private ResourceResolver listenerServiceResourceResolver;
58  
59    private static final String NT_CLIENTLIBRARY = "cq:ClientLibraryFolder";
60    private static final String PN_ALLOWPROXY = "allowProxy";
61  
62    /**
63     * Service user for accessing clientlib resources below /apps and /libs
64     */
65    private static final String CLIENTLIBS_SERVICE = "clientlibs-service";
66  
67    private static final String SERVICE_USER_MAPPING_WARNING = "Missing service user mapping for "
68        + "'io.wcm.handler.url:" + CLIENTLIBS_SERVICE + "' - see https://wcm.io/handler/url/configuration.html";
69  
70    private final LoadingCache<String, ClientlibPathCacheEntry> cache = Caffeine.newBuilder()
71        .maximumSize(10000)
72        .build(path -> {
73          try (ResourceResolver resourceResolver = getServiceResourceResolver()) {
74            Resource resource = resourceResolver.getResource(path);
75            if (resource != null) {
76              Node node = resource.adaptTo(Node.class);
77              if (node != null && node.isNodeType(NT_CLIENTLIBRARY)) {
78                boolean isAllowProxy = resource.getValueMap().get(PN_ALLOWPROXY, false);
79                ClientlibPathCacheEntry entry = new ClientlibPathCacheEntry(path, true, isAllowProxy);
80                log.debug("Detected client library: {}", entry);
81                return entry;
82              }
83            }
84          }
85          catch (LoginException ex) {
86            log.warn(SERVICE_USER_MAPPING_WARNING);
87          }
88          return new ClientlibPathCacheEntry(path, false, false);
89        });
90  
91    private static final Logger log = LoggerFactory.getLogger(ClientlibPathCache.class);
92  
93    ClientlibPathCache(ResourceResolverFactory resourceResolverFactory) {
94      this.resourceResolverFactory = resourceResolverFactory;
95      try {
96        this.listenerServiceResourceResolver = getServiceResourceResolver();
97        enableObservationForClientLibraries();
98      }
99      catch (LoginException ex) {
100       log.warn(SERVICE_USER_MAPPING_WARNING);
101     }
102   }
103 
104   private ResourceResolver getServiceResourceResolver() throws LoginException {
105     return resourceResolverFactory.getServiceResourceResolver(
106         Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object)CLIENTLIBS_SERVICE));
107   }
108 
109   /**
110    * Enable observation in the JCR repository for any events on client library folders.
111    */
112   private void enableObservationForClientLibraries() {
113     Session session = this.listenerServiceResourceResolver.adaptTo(Session.class);
114     if (session != null) {
115       try {
116         log.debug("Enable observation for client libraries.");
117         JackrabbitObservationManager observationManager = (JackrabbitObservationManager)session.getWorkspace().getObservationManager();
118         JackrabbitEventFilter eventFilter = new JackrabbitEventFilter()
119             .setEventTypes(NODE_ADDED | NODE_MOVED | NODE_REMOVED | PROPERTY_ADDED | PROPERTY_CHANGED | PROPERTY_REMOVED)
120             .setAbsPath("/apps")
121             .setAdditionalPaths("/apps", "/libs")
122             .setIsDeep(true)
123             .setNodeTypes(new String[] { NT_CLIENTLIBRARY });
124         observationManager.addEventListener(this, eventFilter);
125       }
126       catch (RepositoryException ex) {
127         log.warn("Unable to register obervation for client libraries.");
128       }
129     }
130   }
131 
132   /**
133    * If any event on any client library occurs clear the client library cache.
134    */
135   @Override
136   public void onEvent(EventIterator events) {
137     log.debug("Clear client library path cache.");
138     cache.invalidateAll();
139   }
140 
141   /**
142    * Checks if the given path is a client library, and if this has enabled the "allowProxy" mode.
143    * @param path Path of a (potential) client library
144    * @return true if it is a client library, and if it has set the "allowProxy" flag.
145    */
146   public boolean isClientlibWithAllowProxy(String path) {
147     ClientlibPathCacheEntry entry = cache.get(path);
148     return entry.isClientLibrary() && entry.isAllowProxy();
149   }
150 
151   @Override
152   public void close() {
153     if (this.listenerServiceResourceResolver != null) {
154       log.debug("End observation for client libraries.");
155       this.listenerServiceResourceResolver.close();
156     }
157   }
158 
159 }