org.ops4j.pax.web.resources.extender.internal.IndexedOsgiResourceLocator.java Source code

Java tutorial

Introduction

Here is the source code for org.ops4j.pax.web.resources.extender.internal.IndexedOsgiResourceLocator.java

Source

/* Copyright 2016 Marc Schlegel
*
* Licensed 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.ops4j.pax.web.resources.extender.internal;

import java.net.URL;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.ops4j.pax.web.resources.api.OsgiResourceLocator;
import org.ops4j.pax.web.resources.api.ResourceInfo;
import org.ops4j.pax.web.resources.api.query.ResourceQueryMatcher;
import org.ops4j.pax.web.resources.api.query.ResourceQueryResult;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * Default implementation of {@link OsgiResourceLocator}.
 * </p>
 * <p>
 * This implementation will lookup all resources under
 * {@code /META-INF/resources/} and put the URL of that files in a map with the
 * key being the same as the resource would be looked up.
 * </p>
 * <p>
 * Furthermore it provides the capability to overwrite resources: if a key in
 * the map is already used, the old value will be moved to a separated
 * collection, until the overriding bundle is stopped.
 * </p>
 *
 * @author Marc Schlegel
 */
public class IndexedOsgiResourceLocator implements OsgiResourceLocator {
    /**
     * Following Servlet 3.0 Specification for JAR-Resources
     */
    private static final String RESOURCE_ROOT = "/META-INF/resources/";

    private BundleContext context;
    private ResourceBundleIndex index;
    private transient Logger logger;

    private List<ResourceBundleIndexEntry> shadowedMap = new CopyOnWriteArrayList<>();

    public IndexedOsgiResourceLocator(BundleContext context) {
        this.logger = LoggerFactory.getLogger(getClass());
        this.context = context;
        index = new ResourceBundleIndex();
    }

    @Override
    public void register(final Bundle bundle) {
        Collection<URL> urls;
        try {
            urls = Collections.list(bundle.findEntries(RESOURCE_ROOT, "*.*", true));
        } catch (IllegalStateException e) {
            logger.error("Error retrieving bundle-resources from bundle '{}'", bundle.getSymbolicName(), e);
            urls = Collections.emptyList();
        }

        urls.forEach(
                url -> index.addResourceToIndex(url.getPath(),
                        new ResourceInfo(url, LocalDateTime
                                .ofInstant(Instant.ofEpochMilli(bundle.getLastModified()), ZoneId.systemDefault()),
                                bundle.getBundleId()),
                        bundle));

        logger.info("Bundle '{}' scanned for resources in '{}': {} entries added to index.",
                new Object[] { bundle.getSymbolicName(), RESOURCE_ROOT, urls.size() });
    }

    @Override
    public void unregister(Bundle bundle) {
        index.cleanBundleFromIndex(bundle);
    }

    @Override
    public ResourceInfo locateResource(String resourceName) {
        final String lookupString = RESOURCE_ROOT + cleanSlashesFromPath(resourceName);

        return index.getResourceInfo(lookupString);
    }

    @Override
    public <R extends ResourceQueryResult, Q extends ResourceQueryMatcher> Collection<R> findResources(
            Q queryMatcher) {
        if (queryMatcher == null) {
            throw new IllegalArgumentException("findResources must be called with non-null queryMatcher!");
        }
        return index.findResources(queryMatcher);
    }

    /**
     * Removes the leading '/' because it does not have any meaning
     * (with and without should point to the same resource)
     *
     * @param path the resource-path to clean
     * @return resource-path without leading '/'
     */
    private String cleanSlashesFromPath(final String path) {
        if (path == null) {
            throw new IllegalArgumentException("createResource must be called with non-null resourceName!");
        }
        String workPath = path;
        if (workPath.charAt(0) == '/') {
            workPath = path.substring(1);
        }
        if (workPath.charAt(path.length() - 1) == '/') {
            workPath = path.substring(0, path.length() - 1);
        }
        return workPath;
    }

    private class ResourceBundleIndex {

        private Map<String, ResourceBundleIndexEntry> indexMap = new ConcurrentHashMap<>(100);

        private void addResourceToIndex(String lookupPath, ResourceInfo resourceInfo, Bundle bundleWithResource) {
            if (StringUtils.isBlank(lookupPath) || resourceInfo == null || bundleWithResource == null) {
                return;
            }
            if (indexMap.containsKey(lookupPath)) {
                ResourceBundleIndexEntry entry = indexMap.get(lookupPath);
                Bundle currentlyProvidingBundle = context.getBundle(entry.getResourceInfo().getBundleId());
                logger.warn(
                        "Resource with path '{}' is already provided by bundle '{}'! Will be overridden by bundle '{}'",
                        new Object[] { lookupPath, currentlyProvidingBundle.getSymbolicName(),
                                bundleWithResource.getSymbolicName() });
                shadowedMap.add(indexMap.get(lookupPath));
            }
            indexMap.put(lookupPath, new ResourceBundleIndexEntry(lookupPath, resourceInfo));
        }

        private ResourceInfo getResourceInfo(String lookupPath) {
            ResourceBundleIndexEntry entry = indexMap.get(lookupPath);
            return entry != null ? entry.getResourceInfo() : null;
        }

        private <R extends ResourceQueryResult, Q extends ResourceQueryMatcher> Collection<R> findResources(
                Q query) {
            List<R> resultList = new ArrayList<>();
            for (Entry<String, ResourceBundleIndexEntry> entry : indexMap.entrySet()) {
                Optional<R> isQueryResult = query.matches(entry.getKey());
                if (isQueryResult.isPresent()) {
                    R queryResult = isQueryResult.get();
                    queryResult.addMatchedResourceInfo(entry.getValue().getResourceInfo());
                    resultList.add(queryResult);
                }
            }
            return Collections.unmodifiableCollection(resultList);
        }

        private void cleanBundleFromIndex(final Bundle bundle) {
            final long removedBundleId = bundle.getBundleId();
            // We first have to collect all necessary entries from the
            // indexMap as well as the shadowedMap into a separated list in
            // order to avoid concurrent-modifications.
            // This is especially important when using streams, because the
            // underlying backing-collection must not be modified while the
            // stream is open (same applies to iterators)
            List<ResourceBundleIndexEntry> entriesToBeRemoved = indexMap.values().stream()
                    .filter(indexEntry -> indexEntry.getResourceInfo().getBundleId() == removedBundleId)
                    .collect(Collectors.toList());
            List<ResourceBundleIndexEntry> entriesToBeRevoked = shadowedMap.stream()
                    .filter(shadowedEntry -> indexMap.containsKey(shadowedEntry.getLookupPath()))
                    .collect(Collectors.toList());
            // remove the entries from the bundle which got stopped
            entriesToBeRemoved.forEach(entry -> indexMap.remove(entry.getLookupPath()));
            logger.info("Removed all resources from bundle '{}'", bundle.getSymbolicName());
            // revoke the matching shadowed-entries back to the indexMap
            entriesToBeRevoked.forEach(entry -> {
                indexMap.put(entry.getLookupPath(), entry);
                logger.info("Revoking shadowed resource '{}' from bundle '{}'", entry.getLookupPath(),
                        context.getBundle(entry.getResourceInfo().getBundleId()).getSymbolicName());
                shadowedMap.remove(entry);
            });
        }
    }

    private static class ResourceBundleIndexEntry {

        private String lookupPath;
        private ResourceInfo resourceInfo;

        private ResourceBundleIndexEntry(final String lookupPath, final ResourceInfo resourceInfo) {
            this.lookupPath = lookupPath;
            this.resourceInfo = resourceInfo;
        }

        private String getLookupPath() {
            return lookupPath;
        }

        private ResourceInfo getResourceInfo() {
            return resourceInfo;
        }
    }
}