/*
* 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.installer.factory.packages.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
import javax.jcr.Session;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.packaging.Dependency;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.Packaging;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.tasks.ChangeStateTask;
import org.apache.sling.installer.api.tasks.InstallTask;
import org.apache.sling.installer.api.tasks.InstallTaskFactory;
import org.apache.sling.installer.api.tasks.InstallationContext;
import org.apache.sling.installer.api.tasks.RegisteredResource;
import org.apache.sling.installer.api.tasks.ResourceState;
import org.apache.sling.installer.api.tasks.ResourceTransformer;
import org.apache.sling.installer.api.tasks.RetryHandler;
import org.apache.sling.installer.api.tasks.TaskResource;
import org.apache.sling.installer.api.tasks.TaskResourceGroup;
import org.apache.sling.installer.api.tasks.TransformationResult;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.framework.Version;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The package transformer:
* <ul>
* <li>detects content packages (ResourceTransformer)
* <li>and creates tasks for installing / removing of content packages
* </ul>
*/
@Component( service = {ResourceTransformer.class, InstallTaskFactory.class})
public class PackageTransformer implements ResourceTransformer, InstallTaskFactory {
/** The attribute holding the package id. */
private static final String ATTR_PCK_ID = "package-id";
/** The resource type for packages. */
private static final String RESOURCE_TYPE = "content-package";
/**
* The logger.
*/
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Reference
private SlingRepository repository;
@Reference
private Packaging pkgSvc;
@Reference
private RetryHandler retryHandler;
private PackageTransformerConfiguration configuration;
@Activate
private void activate(final PackageTransformerConfiguration configuration) {
this.configuration = configuration;
}
/**
* @see org.apache.sling.installer.api.tasks.ResourceTransformer#transform(org.apache.sling.installer.api.tasks.RegisteredResource)
*/
@Override
public TransformationResult[] transform(final RegisteredResource resource) {
if (resource.getType().equals(InstallableResource.TYPE_FILE)) {
return checkForPackage(resource);
}
return null;
}
/**
* Check if the resource is a content package
* @param resource The resource
* @return {@code null} if not a content package, a result otherwise
*/
private TransformationResult[] checkForPackage(final RegisteredResource resource) {
// first check if this is a zip archive
try (final ZipInputStream zin = new ZipInputStream(new BufferedInputStream(resource.getInputStream()))) {
if (zin.getNextEntry() == null) {
return null;
}
} catch (final IOException ioe) {
logger.debug("Unable to read resource.", ioe);
return null;
}
Session session = null;
JcrPackage pck = null;
try {
// create an admin session
session = repository.loginAdministrative(null);
final JcrPackageManager pckMgr = pkgSvc.getPackageManager(session);
pck = pckMgr.upload(resource.getInputStream(), true, true);
if (pck.isValid()) {
final PackageId pid = pck.getDefinition().getId();
final Map<String, Object> attrs = new HashMap<String, Object>();
attrs.put(ATTR_PCK_ID, pid.toString());
final TransformationResult tr = new TransformationResult();
tr.setId(pid.getGroup() + ':' + pid.getName());
tr.setResourceType(RESOURCE_TYPE);
tr.setAttributes(attrs);
// version
final String version = pid.getVersionString();
if ( version.length() > 0 ) {
tr.setVersion(new Version(cleanupVersion(version)));
}
return new TransformationResult[] {tr};
}
} catch (final Exception ioe) {
logger.debug("Unable to check content package " + resource.getURL(), ioe);
} finally {
if (pck != null) {
pck.close();
}
if (session != null) {
session.logout();
}
}
return null;
}
/**
* @see org.apache.sling.installer.api.tasks.InstallTaskFactory#createTask(org.apache.sling.installer.api.tasks.TaskResourceGroup)
*/
@Override
public InstallTask createTask(final TaskResourceGroup toActivate) {
final TaskResource resource = toActivate.getActiveResource();
if (resource == null || !resource.getType().equals(RESOURCE_TYPE)) {
return null;
}
// extract the package id
final String id = (String)resource.getAttribute(ATTR_PCK_ID);
final PackageId pkgId = PackageId.fromString(id);
if (pkgId == null) {
logger.error("Error during processing of {}: Package id is wrong/null.", resource);
return new ChangeStateTask(toActivate, ResourceState.IGNORED);
}
if (resource.getState() == ResourceState.INSTALL) {
return new InstallPackageTask(pkgId, toActivate);
}
return new UninstallPackageTask(pkgId, toActivate);
}
/**
* Task for installing a package.
*/
private class InstallPackageTask extends InstallTask {
private final PackageId pkgId;
public InstallPackageTask(final PackageId pkgId, final TaskResourceGroup erl) {
super(erl);
this.pkgId = pkgId;
}
@Override
public void execute(final InstallationContext ctx) {
final TaskResource resource = this.getResource();
// now check the dependencies
Session session = null;
JcrPackage pkg = null;
try {
session = repository.loginAdministrative(null);
final JcrPackageManager pkgMgr = pkgSvc.getPackageManager(session);
// open package
pkg = pkgMgr.open(pkgId);
if (pkg == null) {
String message = MessageFormat.format("Error during installation of {0}: Package {1} missing.", resource, pkgId);
logger.error(message);
this.setFinishedState(ResourceState.IGNORED, null, message);
return;
}
// if this is a SNAPSHOT version we always trigger a reinstall
// (this workaround can be removed once https://issues.apache.org/jira/browse/JCRVLT-155 is implemented)
if (!pkgId.getVersionString().endsWith("-SNAPSHOT")) {
// check if package was installed previously by some other means (or even by a previous run of the installer)
if (pkg.isInstalled()) {
String message = MessageFormat.format("Package {0} was installed externally. Marking as installed.", pkgId);
logger.info(message);
this.setFinishedState(ResourceState.INSTALLED, null, message);
return;
}
}
// check if dependencies are installed
for (final Dependency d : pkg.getDefinition().getDependencies()) {
if (pkgMgr.resolve(d, true) == null) {
logger.info("Delaying installation of {} due to missing dependency {}.", pkgId, d);
return;
}
}
// finally, install package
final ImportOptions opts = new ImportOptions();
if (configuration.shouldCreateSnapshots()) {
pkg.install(opts);
ctx.log("Content package installed: {}", resource);
} else {
pkg.extract(opts);
ctx.log("Content package extracted: {}", resource);
}
setFinishedState(ResourceState.INSTALLED);
// notify retry handler to install dependend packages.
retryHandler.scheduleRetry();
} catch (final Exception e) {
String message = MessageFormat.format("Error while processing install task of {0} due to {1}, no retry.", resource, e.getLocalizedMessage());
logger.error(message, e);
this.setFinishedState(ResourceState.IGNORED, null, message);
} finally {
if (pkg != null) {
pkg.close();
}
if (session != null) {
session.logout();
}
}
}
@Override
public String getSortKey() {
return "25-" + getResource().getEntityId();
}
}
/**
* Task for uninstalling a package.
*/
private final class UninstallPackageTask extends InstallTask {
private final PackageId pkgId;
public UninstallPackageTask(final PackageId pkgId, final TaskResourceGroup erl) {
super(erl);
this.pkgId = pkgId;
}
@Override
public void execute(final InstallationContext ctx) {
Session session = null;
JcrPackage pkg = null;
try {
session = repository.loginAdministrative(null);
final JcrPackageManager pkgMgr = pkgSvc.getPackageManager(session);
pkg = pkgMgr.open(this.pkgId);
if ( pkg != null ) {
final ImportOptions opts = new ImportOptions();
pkg.uninstall(opts);
}
} catch (final Exception e) {
logger.error("Error while processing uninstall task of {}.", pkgId, e);
} finally {
if (pkg != null) {
pkg.close();
}
if (session != null) {
session.logout();
}
}
ctx.log("Uninstalled content package {}", getResource());
setFinishedState(ResourceState.UNINSTALLED);
retryHandler.scheduleRetry();
}
@Override
public String getSortKey() {
return "55-" + getResource().getEntityId();
}
}
private static final Pattern FUZZY_VERSION = Pattern.compile( "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
Pattern.DOTALL );
/**
* Clean up version parameters. Other builders use more fuzzy definitions of
* the version syntax. This method cleans up such a version to match an OSGi
* version.
*
* @param version The version string to clean up
* @return the clean version
*/
private static String cleanupVersion(final String version) {
final StringBuilder result = new StringBuilder();
final Matcher m = FUZZY_VERSION.matcher( version );
if ( m.matches() ) {
final String major = m.group( 1 );
final String minor = m.group( 3 );
final String micro = m.group( 5 );
final String qualifier = m.group( 7 );
if ( major != null ) {
result.append( major );
if ( minor != null ) {
result.append( "." );
result.append( minor );
if ( micro != null ) {
result.append( "." );
result.append( micro );
if ( qualifier != null ) {
result.append( "." );
cleanupModifier( result, qualifier );
}
} else if ( qualifier != null ) {
result.append( ".0." );
cleanupModifier( result, qualifier );
} else {
result.append( ".0" );
}
} else if ( qualifier != null ) {
result.append( ".0.0." );
cleanupModifier( result, qualifier );
} else {
result.append( ".0.0" );
}
}
} else {
result.append( "0.0.0." );
cleanupModifier( result, version );
}
return result.toString();
}
private static void cleanupModifier( final StringBuilder result, final String modifier ) {
for ( int i = 0; i < modifier.length(); i++ ) {
char c = modifier.charAt( i );
if ( ( c >= '0' && c <= '9' )
|| ( c >= 'a' && c <= 'z' )
|| ( c >= 'A' && c <= 'Z' )
|| c == '_'
|| c == '-' ) {
result.append( c );
} else {
result.append( '_' );
}
}
}
}