blob: d8160e9f0ae7c14168745f8f108c72bb60b8a845 [file] [log] [blame]
/*
* 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.jcr.contentloader.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import javax.jcr.Item;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.version.VersionManager;
import org.apache.commons.io.IOUtils;
import org.apache.sling.jcr.contentloader.ContentReader;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>BundleContentLoader</code> loads initial content from the bundle.
*/
public class BundleContentLoader extends BaseImportLoader {
public static final String PARENT_DESCRIPTOR = "ROOT";
private final Logger log = LoggerFactory.getLogger(BundleContentLoader.class);
private BundleHelper bundleHelper;
// bundles whose registration failed and should be retried
private List<Bundle> delayedBundles;
private Set<String> runmodes;
public BundleContentLoader(BundleHelper bundleHelper, ContentReaderWhiteboard contentReaderWhiteboard, Set<String> runmodes) {
super(contentReaderWhiteboard);
this.bundleHelper = bundleHelper;
this.delayedBundles = new LinkedList<>();
this.runmodes = runmodes;
}
public void dispose() {
if (delayedBundles != null) {
delayedBundles.clear();
delayedBundles = null;
}
bundleHelper = null;
}
/**
* Register a bundle and install its content.
*
* @param metadataSession
* @param bundle
* @throws RepositoryException
*/
public void registerBundle(final Session metadataSession, final Bundle bundle, final boolean isUpdate) {
// if this is an update, we have to uninstall the old content first
if (isUpdate) {
this.unregisterBundle(metadataSession, bundle);
}
log.debug("Registering bundle {} for content loading.", bundle.getSymbolicName());
if (registerBundleInternal(metadataSession, bundle, false, isUpdate)) {
// handle delayed bundles, might help now
int currentSize = -1;
for (int i = delayedBundles.size(); i > 0 && currentSize != delayedBundles.size() && !delayedBundles.isEmpty(); i--) {
for (Iterator<Bundle> di = delayedBundles.iterator(); di.hasNext(); ) {
Bundle delayed = di.next();
if (registerBundleInternal(metadataSession, delayed, true, false)) {
di.remove();
}
}
currentSize = delayedBundles.size();
}
} else if (!isUpdate) {
// add to delayed bundles - if this is not an update!
delayedBundles.add(bundle);
}
}
private boolean registerBundleInternal(final Session metadataSession, final Bundle bundle, final boolean isRetry, final boolean isUpdate) {
// check if bundle has initial content
final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle);
if (pathIter == null) {
log.debug("Bundle {} has no initial content", bundle.getSymbolicName());
return true;
}
try {
bundleHelper.createRepositoryPath(metadataSession, ContentLoaderService.BUNDLE_CONTENT_NODE);
// check if the content has already been loaded
final Map<String, Object> bundleContentInfo = bundleHelper.getBundleContentInfo(metadataSession, bundle, true);
// if we don't get an info, someone else is currently loading
if (bundleContentInfo == null) {
return false;
}
boolean success = false;
List<String> createdNodes = null;
try {
final boolean contentAlreadyLoaded = ((Boolean) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED)).booleanValue();
boolean isBundleUpdated = false;
Calendar lastLoadedAt = (Calendar) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED_AT);
if (lastLoadedAt != null && lastLoadedAt.getTimeInMillis() < bundle.getLastModified()) {
isBundleUpdated = true;
}
if (!isUpdate && !isBundleUpdated && contentAlreadyLoaded) {
log.info("Content of bundle already loaded {}.", bundle.getSymbolicName());
} else {
createdNodes = installContent(metadataSession, bundle, pathIter, contentAlreadyLoaded && !isBundleUpdated);
if (isRetry) {
// log success of retry
log.info("Retrying to load initial content for bundle {} succeeded.", bundle.getSymbolicName());
}
}
success = true;
return true;
} finally {
bundleHelper.unlockBundleContentInfo(metadataSession, bundle, success, createdNodes);
}
} catch (RepositoryException re) {
// if we are retrying we already logged this message once, so we
// won't log it again
if (!isRetry) {
log.error("Cannot load initial content for bundle " + bundle.getSymbolicName() + " : " + re.getMessage(), re);
}
}
return false;
}
/**
* Unregister a bundle. Remove installed content.
*
* @param bundle The bundle.
*/
public void unregisterBundle(final Session session, final Bundle bundle) {
if (delayedBundles.contains(bundle)) {
delayedBundles.remove(bundle);
} else {
try {
bundleHelper.createRepositoryPath(session, ContentLoaderService.BUNDLE_CONTENT_NODE);
final Map<String, Object> bundleContentInfo = bundleHelper.getBundleContentInfo(session, bundle, false);
// if we don't get an info, someone else is currently loading or unloading
// or the bundle is already uninstalled
if (bundleContentInfo == null) {
return;
}
try {
uninstallContent(session, bundle, (String[]) bundleContentInfo.get(ContentLoaderService.PROPERTY_UNINSTALL_PATHS));
bundleHelper.contentIsUninstalled(session, bundle);
} finally {
bundleHelper.unlockBundleContentInfo(session, bundle, false, null);
}
} catch (RepositoryException re) {
log.error("Cannot remove initial content for bundle " + bundle.getSymbolicName() + " : " + re.getMessage(), re);
}
}
}
// ---------- internal -----------------------------------------------------
/**
* Install the content from the bundle.
*
* @return If the content should be removed on uninstall, a list of top nodes
*/
private List<String> installContent(final Session defaultSession, final Bundle bundle, final Iterator<PathEntry> pathIter, final boolean contentAlreadyLoaded) throws RepositoryException {
final List<String> createdNodes = new ArrayList<>();
final Map<String, Session> createdSessions = new HashMap<>();
log.debug("Installing initial content from bundle {}", bundle.getSymbolicName());
final DefaultContentCreator contentCreator = new DefaultContentCreator(this.bundleHelper);
try {
while (pathIter.hasNext()) {
final PathEntry pathEntry = pathIter.next();
if (validRunmode(pathEntry) && (!contentAlreadyLoaded || pathEntry.isOverwrite())) {
String workspace = pathEntry.getWorkspace();
final Session targetSession;
if (workspace != null) {
if (createdSessions.containsKey(workspace)) {
targetSession = createdSessions.get(workspace);
} else {
targetSession = createSession(workspace);
createdSessions.put(workspace, targetSession);
}
} else {
targetSession = defaultSession;
}
final Node targetNode = getTargetNode(targetSession, pathEntry.getTarget());
if (targetNode != null) {
installFromPath(bundle, pathEntry.getPath(), pathEntry, targetNode, pathEntry.isUninstall() ? createdNodes : null, contentCreator);
}
}
}
// now optimize created nodes list
Collections.sort(createdNodes);
if (createdNodes.size() > 1) {
final Iterator<String> i = createdNodes.iterator();
String previous = i.next() + '/';
while (i.hasNext()) {
final String current = i.next();
if (current.startsWith(previous)) {
i.remove();
} else {
previous = current + '/';
}
}
}
// persist modifications now
defaultSession.refresh(true);
defaultSession.save();
for (Session session : createdSessions.values()) {
session.refresh(true);
session.save();
}
// finally check in versionable nodes
for (final Node versionable : contentCreator.getVersionables()) {
VersionManager versionManager = versionable.getSession().getWorkspace().getVersionManager();
versionManager.checkin(versionable.getPath());
}
} finally {
try {
if (defaultSession.hasPendingChanges()) {
defaultSession.refresh(false);
}
for (Session session : createdSessions.values()) {
if (session.hasPendingChanges()) {
session.refresh(false);
}
}
} catch (RepositoryException re) {
log.warn("Failure to rollback partial initial content for bundle {}", bundle.getSymbolicName(), re);
}
contentCreator.clear();
for (Session session : createdSessions.values()) {
session.logout();
}
}
log.debug("Done installing initial content from bundle {}", bundle.getSymbolicName());
return createdNodes;
}
/**
* Checks if the path entry has a runmode restriction set and the runmode isn't set in the Sling instance.
*
* @param pathEntry the path entry to check
* @return true if the required runmode setting is not set or the instance runmodes doesn't contain the runmode
*/
private boolean validRunmode(PathEntry pathEntry) {
return pathEntry.getSkipRunmode() == null || "".equals(pathEntry.getSkipRunmode())
|| !runmodes.contains(pathEntry.getSkipRunmode());
}
/**
* Handle content installation for a single path.
*
* @param bundle The bundle containing the content.
* @param path The path
* @param configuration
* @param parent The parent node.
* @param createdNodes An optional list to store all new nodes. This list is
* used for an uninstall
* @throws RepositoryException
*/
private void installFromPath(final Bundle bundle, final String path, final PathEntry configuration, final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator) throws RepositoryException {
// init content creator
contentCreator.init(configuration, getContentReaders(), createdNodes, null);
final Map<String, Node> processedEntries = new HashMap<>();
Enumeration<String> entries = bundle.getEntryPaths(path);
if (entries == null) {
// check for single content
final URL u = bundle.getEntry(path);
if (u == null) {
log.info("install: No initial content entries at {} in bundle {}", path, bundle.getSymbolicName());
return;
}
// we have a single file content, let's check if this has an content reader extension
for (String ext : contentCreator.getContentReaders().keySet()) {
if (path.endsWith(ext)) {
}
}
handleFile(path, bundle, processedEntries, configuration, parent, createdNodes, contentCreator);
return;
}
// potential parent node import/extension
URL parentNodeDescriptor = importParentNode(bundle, path, parent, contentCreator);
if (parentNodeDescriptor != null) {
processedEntries.put(parentNodeDescriptor.toString(), parent);
}
while (entries.hasMoreElements()) {
final String entry = entries.nextElement();
log.debug("Processing initial content entry {} in bundle {}", entry, bundle.getSymbolicName());
if (entry.endsWith("/")) {
// dir, check for node descriptor, else create dir
final String base = entry.substring(0, entry.length() - 1);
URL nodeDescriptor = null;
for (String ext : contentCreator.getContentReaders().keySet()) {
nodeDescriptor = bundle.getEntry(base + ext);
if (nodeDescriptor != null) {
break;
}
}
// if we have a descriptor, which has not been processed yet,
// otherwise call createFolder, which creates an nt:folder or
// returns an existing node (created by a descriptor)
final String name = getName(base);
Node node = null;
if (nodeDescriptor != null) {
node = processedEntries.get(nodeDescriptor.toString());
if (node == null) {
node = createNode(parent, name, nodeDescriptor, contentCreator, configuration);
processedEntries.put(nodeDescriptor.toString(), node);
}
} else {
node = createFolder(parent, name, configuration.isOverwrite());
}
// walk down the line
if (node != null) {
installFromPath(bundle, entry, configuration, node, createdNodes, contentCreator);
}
} else {
// file => create file
handleFile(entry, bundle, processedEntries, configuration, parent, createdNodes, contentCreator);
}
}
}
/**
* Handle a file entry.
*
* @param entry
* @param bundle
* @param processedEntries
* @param configuration
* @param parent
* @param createdNodes
* @throws RepositoryException
*/
private void handleFile(final String entry, final Bundle bundle, final Map<String, Node> processedEntries, final PathEntry configuration, final Node parent, final List<String> createdNodes, final DefaultContentCreator contentCreator) throws RepositoryException {
final URL file = bundle.getEntry(entry);
final String name = getName(entry);
try {
if (processedEntries.containsKey(file.toString())) {
// this is a consumed node descriptor
return;
}
// check for node descriptor
URL nodeDescriptor = null;
for (String ext : contentCreator.getContentReaders().keySet()) {
nodeDescriptor = bundle.getEntry(entry + ext);
if (nodeDescriptor != null) {
break;
}
}
// install if it is a descriptor
boolean foundReader = getContentReader(entry, configuration) != null;
Node node = null;
if (foundReader) {
node = createNode(parent, name, file, contentCreator, configuration);
if (node != null) {
log.debug("Created node as {} {}", node.getPath(), name);
processedEntries.put(file.toString(), node);
} else {
log.warn("No node created for file {} {}", file, name);
}
} else {
log.debug("Can't find content reader for entry {} at {}", entry, name);
}
// otherwise just place as file
if (node == null) {
try {
createFile(configuration, parent, file, createdNodes, contentCreator);
node = parent.getNode(name);
} catch (IOException ioe) {
log.warn("Cannot create file node for {}", file, ioe);
}
}
// if we have a descriptor, which has not been processed yet,
// process it
if (nodeDescriptor != null && !processedEntries.containsKey(nodeDescriptor.toString())) {
try {
contentCreator.setIgnoreOverwriteFlag(true);
node = createNode(parent, name, nodeDescriptor, contentCreator, configuration);
processedEntries.put(nodeDescriptor.toString(), node);
} finally {
contentCreator.setIgnoreOverwriteFlag(false);
}
}
} catch (RepositoryException e) {
log.error("Failed to process file {} from {}", file, name);
throw e;
}
}
/**
* Create a new node from a content resource found in the bundle.
*
* @param parent The parent node
* @param name The name of the new content node
* @param resourceUrl The resource url.
* @param contentCreator the content creator
* @param configuration the configuration for the node that needs to be created
* @return
* @throws RepositoryException
*/
private Node createNode(Node parent, String name, URL resourceUrl, final DefaultContentCreator contentCreator, PathEntry configuration)
throws RepositoryException {
final String resourcePath = resourceUrl.getPath().toLowerCase();
InputStream contentStream = null;
try {
// special treatment for system view imports
if (resourcePath.endsWith(EXT_JCR_XML)) {
contentStream = resourceUrl.openStream();
return importJcrXml(parent, name, contentStream, false);
}
// get the node reader for this resource
final ContentReader nodeReader = getContentReader(resourcePath, configuration);
// cannot find out the type
if (nodeReader == null) {
return null;
}
final String contentReaderExtension = getContentReaderExtension(name);
contentCreator.prepareParsing(parent, toPlainName(name, contentReaderExtension));
nodeReader.parse(resourceUrl, contentCreator);
return contentCreator.getCreatedRootNode();
} catch (RepositoryException re) {
throw re;
} catch (Exception t) {
throw new RepositoryException(t.getMessage(), t);
} finally {
IOUtils.closeQuietly(contentStream);
}
}
/**
* Create a folder
*
* @param parent The parent node.
* @param name The name of the folder
* @param overwrite If set to true, an existing folder is removed first.
* @return The node pointing to the folder.
* @throws RepositoryException
*/
private Node createFolder(Node parent, String name, final boolean overwrite) throws RepositoryException {
if (parent.hasNode(name)) {
if (overwrite) {
parent.getNode(name).remove();
} else {
return parent.getNode(name);
}
}
return parent.addNode(name, "sling:Folder");
}
/**
* Create a file from the given url.
*
* @param configuration
* @param parent
* @param source
* @param createdNodes
* @param contentCreator
* @throws IOException
* @throws RepositoryException
*/
private void createFile(PathEntry configuration, Node parent, URL source, List<String> createdNodes, final DefaultContentCreator contentCreator) throws IOException, RepositoryException {
final String srcPath = source.getPath();
int pos = srcPath.lastIndexOf('/');
final String name = getName(source.getPath());
final String path;
if (pos == -1) {
path = name;
} else {
path = srcPath.substring(0, pos + 1) + name;
}
contentCreator.init(configuration, getContentReaders(), createdNodes, null);
contentCreator.prepareParsing(parent, name);
final URLConnection conn = source.openConnection();
final long lastModified = Math.min(conn.getLastModified(), configuration.getLastModified());
final String type = conn.getContentType();
final InputStream data = conn.getInputStream();
contentCreator.createFileAndResourceNode(path, data, type, lastModified);
contentCreator.finishNode();
contentCreator.finishNode();
}
/**
* Gets and decodes the name part of the <code>path</code>. The name is
* the part of the path after the last slash (or the complete path if no
* slash is contained). To support names containing unsupported characters
* such as colon (<code>:</code>), names may be URL encoded (see
* <code>java.net.URLEncoder</code>) using the <i>UTF-8</i> character
* encoding. In this case, this method decodes the name using the
* <code>java.net.URLDecoder</code> class with the <i>UTF-8</i> character
* encoding.
*
* @param path The path from which to extract the name part.
* @return The URL decoded name part.
*/
private String getName(String path) {
int lastSlash = path.lastIndexOf('/');
String name = (lastSlash < 0) ? path : path.substring(lastSlash + 1);
// check for encoded characters (%xx)
// has encoded characters, need to decode
if (name.indexOf('%') >= 0) {
try {
return URLDecoder.decode(name, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// actually unexpected because UTF-8 is required by the spec
log.error("Cannot decode " + name + " because the platform has no support for UTF-8, using undecoded");
} catch (Exception e) {
// IllegalArgumentException or failure to decode
log.error("Cannot decode " + name + ", using undecoded", e);
}
}
// not encoded or problems decoding, return the name unmodified
return name;
}
private Node getTargetNode(Session session, String path) throws RepositoryException {
// not specified path directive
if (path == null) {
return session.getRootNode();
}
if (!path.startsWith("/")) {
// make relative path absolute
path = "/" + path;
}
if (!session.itemExists(path)) {
Node currentNode = session.getRootNode();
final StringTokenizer st = new StringTokenizer(path.substring(1), "/");
while (st.hasMoreTokens()) {
final String name = st.nextToken();
if (!currentNode.hasNode(name)) {
currentNode.addNode(name, "sling:Folder");
}
currentNode = currentNode.getNode(name);
}
return currentNode;
}
Item item = session.getItem(path);
return (item.isNode()) ? (Node) item : null;
}
private void uninstallContent(final Session defaultSession, final Bundle bundle, final String[] uninstallPaths) {
final Map<String, Session> createdSessions = new HashMap<>();
try {
log.debug("Uninstalling initial content from bundle {}", bundle.getSymbolicName());
if (uninstallPaths != null && uninstallPaths.length > 0) {
for (String path : uninstallPaths) {
final Session targetSession;
final int wsSepPos = path.indexOf(":/");
if (wsSepPos != -1) {
final String workspaceName = path.substring(0, wsSepPos);
path = path.substring(wsSepPos + 1);
if (workspaceName.equals(defaultSession.getWorkspace().getName())) {
targetSession = defaultSession;
} else if (createdSessions.containsKey(workspaceName)) {
targetSession = createdSessions.get(workspaceName);
} else {
targetSession = createSession(workspaceName);
createdSessions.put(workspaceName, targetSession);
}
} else {
targetSession = defaultSession;
}
if (targetSession.itemExists(path)) {
targetSession.getItem(path).remove();
}
}
// persist modifications now
defaultSession.save();
for (Session session : createdSessions.values()) {
session.save();
}
}
log.debug("Done uninstalling initial content from bundle {}", bundle.getSymbolicName());
} catch (RepositoryException re) {
log.error("Unable to uninstall initial content from bundle " + bundle.getSymbolicName(), re);
} finally {
try {
if (defaultSession.hasPendingChanges()) {
defaultSession.refresh(false);
}
for (Session session : createdSessions.values()) {
if (session.hasPendingChanges()) {
session.refresh(false);
}
}
} catch (RepositoryException re) {
log.warn("Failure to rollback uninstalling initial content for bundle {}", bundle.getSymbolicName(), re);
}
for (Session session : createdSessions.values()) {
session.logout();
}
}
}
protected static final class Descriptor {
public URL url;
private ContentReader contentReader;
}
/**
* Return the parent node descriptor (ROOT).
*/
private Descriptor getParentNodeDescriptor(final Bundle bundle, final String path, final DefaultContentCreator contentCreator) {
for (Map.Entry<String, ContentReader> entry : contentCreator.getContentReaders().entrySet()) {
if (entry.getValue() != null) {
final StringBuilder filePath = new StringBuilder(path);
if (!path.endsWith("/")) {
filePath.append("/");
}
filePath.append(PARENT_DESCRIPTOR);
// add file extension, e.g. .jcr.xml, .xml, .zip (see BaseImportLoader)
filePath.append(entry.getKey());
URL url = bundle.getEntry(filePath.toString());
if (url != null) {
final Descriptor descriptor = new Descriptor();
descriptor.url = url;
descriptor.contentReader = entry.getValue();
return descriptor;
}
}
}
return null;
}
/**
* Imports mixin nodes and properties (and optionally child nodes) of the
* parent node.
*/
private URL importParentNode(Bundle bundle, String path, Node parent, final DefaultContentCreator contentCreator) throws RepositoryException {
final Descriptor descriptor = getParentNodeDescriptor(bundle, path, contentCreator);
// no parent descriptor (ROOT) found
if (descriptor == null) {
return null;
}
try {
contentCreator.prepareParsing(parent, null);
descriptor.contentReader.parse(descriptor.url, contentCreator);
return descriptor.url;
} catch (RepositoryException re) {
throw re;
} catch (Exception t) {
throw new RepositoryException(t.getMessage(), t);
}
}
private Session createSession(String workspace) throws RepositoryException {
try {
return bundleHelper.getSession(workspace);
} catch (NoSuchWorkspaceException e) {
Session temp = bundleHelper.getSession();
temp.getWorkspace().createWorkspace(workspace);
temp.logout();
return bundleHelper.getSession(workspace);
}
}
}