blob: b94eea75cb3743389c677a862ef73d4e57eb0531 [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.feature.cpconverter.handlers.slinginitialcontent;
import static org.apache.sling.feature.cpconverter.shared.ConverterConstants.SLASH;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.time.ZoneOffset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.value.ValueFactoryImpl;
import org.apache.jackrabbit.vault.fs.io.DocViewFormat;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.DocViewProperty2;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.jackrabbit.vault.util.xml.serialize.FormattingXmlStreamWriter;
import org.apache.sling.feature.cpconverter.vltpkg.JcrNamespaceRegistry;
import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
import org.apache.sling.jcr.contentloader.ContentCreator;
import org.jetbrains.annotations.NotNull;
/**
* ContentCreator implementation to write FileVault enhanced DocView XML files (to be packaged into a VaultPackage)
*/
public class VaultContentXMLContentCreator implements ContentCreator {
private static final String ACL_NOT_SUPPORTED_MSG = "Sling Initial Content - ACL statements are not supported yet . SLING issue: https://issues.apache.org/jira/browse/SLING-11060";
private final VaultPackageAssembler packageAssembler;
private final Queue<DocViewTreeNode> currentNodeStack = Collections.asLifoQueue(new ArrayDeque<>());
private final JcrNamespaceRegistry namespaceRegistry;
private final NamePathResolver npResolver;
private final XMLStreamWriter writer;
private String rootPath;
private final boolean isFileDescriptorEntry;
private boolean isFinished = false;
private DocViewTreeNode rootNode = null;
VaultContentXMLContentCreator(@NotNull String repositoryPath,
@NotNull OutputStream targetOutputStream,
@NotNull JcrNamespaceRegistry namespaceRegistry,
@NotNull VaultPackageAssembler packageAssembler,
boolean isFileDescriptorEntry) throws RepositoryException, FactoryConfigurationError {
this.packageAssembler = packageAssembler;
this.namespaceRegistry = namespaceRegistry;
this.isFileDescriptorEntry = isFileDescriptorEntry;
this.npResolver = new DefaultNamePathResolver((NamespaceRegistry)namespaceRegistry);
try {
writer = FormattingXmlStreamWriter.create(targetOutputStream, new DocViewFormat().getXmlOutputFormat());
} catch (XMLStreamException e) {
throw new RepositoryException("Cannot create XML Writer " + e, e);
}
rootPath = repositoryPath;
}
/**
* The absolute entry path inside the content package ZIP for the generated docview.xml
* @throws NamespaceException
*/
public String getContentPackageEntryPath() throws NamespaceException {
String suffix;
if (isFileDescriptorEntry) {
// it is potentially https://jackrabbit.apache.org/filevault/vaultfs.html#extended-file-aggregates (and may have file name clashes otherwise with the binary file in the content package)
suffix = ".dir" + "/" + Constants.DOT_CONTENT_XML;
} else {
suffix = "/" + Constants.DOT_CONTENT_XML;
}
return SLASH + org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(rootNode.getPath(npResolver)) + suffix;
}
@Override
public void createNode(String name, String primaryNodeType, String[] mixinNodeTypes) throws RepositoryException {
final Name currentNodeName;
if (rootNode == null) {
currentNodeName = NameConstants.JCR_ROOT;
if (StringUtils.isNotBlank(name)) {
// adjust root path in case the first node has an explicit name
rootPath = Text.getRelativeParent(rootPath, 1) + "/" + name;
}
} else {
currentNodeName = npResolver.getQName(name);
}
// if we are dealing with a descriptor file, we should use nt:file as default primaryType.
String defaultNtType = isFileDescriptorEntry ? JcrConstants.NT_FILE : JcrConstants.NT_UNSTRUCTURED;
String toUsePrimaryNodeType = StringUtils.isNotBlank(primaryNodeType) ? primaryNodeType : defaultNtType;
List<DocViewProperty2> currentProperties = new ArrayList<>();
currentProperties.add(new DocViewProperty2(NameConstants.JCR_PRIMARYTYPE, toUsePrimaryNodeType));
if (ArrayUtils.isNotEmpty(mixinNodeTypes)) {
currentProperties.add(new DocViewProperty2(NameConstants.JCR_MIXINTYPES, Arrays.asList(mixinNodeTypes)));
}
final DocViewTreeNode newNode;
if (rootNode == null) {
newNode = new DocViewTreeNode(rootPath, currentNodeName, currentProperties);
rootNode = newNode;
} else {
newNode = new DocViewTreeNode(rootNode.getPath(npResolver), currentNodeName, currentProperties);
currentNodeStack.element().addChild(newNode);
}
currentNodeStack.add(newNode);
}
@Override
public void finishNode() throws RepositoryException {
this.currentNodeStack.remove();
}
@Override
public void finish() throws RepositoryException {
if (isFinished) {
return;
}
isFinished = true;
try {
rootNode.write(writer, namespaceRegistry, Arrays.asList(namespaceRegistry.getPrefixes()));
writer.close();
} catch (XMLStreamException e) {
throw new RepositoryException("Cannot close XML writer " + e, e);
}
}
@Override
public void createProperty(String name, int propertyType, String value) throws RepositoryException {
// add explicit type due to https://issues.apache.org/jira/browse/JCRVLT-693
currentNodeStack.peek().getProperties().add(new DocViewProperty2(npResolver.getQName(name), value, propertyType == PropertyType.UNDEFINED ? PropertyType.STRING : propertyType));
}
@Override
public void createProperty(String name, int propertyType, String[] values) throws RepositoryException {
// add explicit type due to https://issues.apache.org/jira/browse/JCRVLT-693
currentNodeStack.peek().getProperties().add(new DocViewProperty2(npResolver.getQName(name), Arrays.asList(values), propertyType == PropertyType.UNDEFINED ? PropertyType.STRING : propertyType));
}
@Override
public void createProperty(String name, Object value) throws RepositoryException {
// store binaries outside of docview xml
Value jcrValue = createValue(name, value, -1);
DocViewProperty2 property = DocViewProperty2.fromValues(npResolver.getQName(name), new Value[] { jcrValue }, jcrValue.getType(), false, false, false);
currentNodeStack.peek().getProperties().add(property);
}
@Override
public void createProperty(String name, Object[] values) throws RepositoryException {
try {
AtomicInteger index = new AtomicInteger();
Value[] jcrValues = Arrays.stream(values).map(v -> {
try {
return createValue(name,v, index.getAndIncrement());
} catch (RepositoryException e) {
throw new UncheckedRepositoryException(e);
}
}).toArray(Value[]::new);
final int type;
if (jcrValues.length == 0) {
type = PropertyType.STRING;
} else {
type = jcrValues[0].getType();
}
DocViewProperty2 property = DocViewProperty2.fromValues(npResolver.getQName(name), jcrValues, type, true, false, false);
currentNodeStack.peek().getProperties().add(property);
} catch (UncheckedRepositoryException e) {
throw e.getCause();
}
}
static final class UncheckedRepositoryException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UncheckedRepositoryException(RepositoryException e) {
super(e);
}
@Override
public synchronized RepositoryException getCause() {
return (RepositoryException) super.getCause();
}
}
private Value createValue(String name, Object value, int index) throws RepositoryException {
ValueFactory valueFactory = ValueFactoryImpl.getInstance();
final Value jcrValue;
if (value instanceof String) {
jcrValue = valueFactory.createValue((String)value);
} else if (value instanceof Long) {
jcrValue = valueFactory.createValue((long)value);
} else if (value instanceof Double) {
jcrValue = valueFactory.createValue((Double)value);
} else if (value instanceof BigDecimal) {
jcrValue = valueFactory.createValue((BigDecimal)value);
} else if (value instanceof Date) {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC), Locale.ROOT);
calendar.setTime((Date)value);
jcrValue = valueFactory.createValue(calendar);
} else if (value instanceof Calendar) {
jcrValue = valueFactory.createValue((Calendar)value);
} else if (value instanceof Boolean) {
jcrValue = valueFactory.createValue((Boolean)value);
} else if (value instanceof InputStream) {
// binaries are always stored outside the docview xml (https://jackrabbit.apache.org/filevault/vaultfs.html#Binary_Properties)
String binaryPropertyEntryName = PlatformNameFormat.getPlatformName(name) + ((index != -1) ? "[" + index + "]" : "") + ".binary";
createBinary((InputStream)value, SLASH + binaryPropertyEntryName);
jcrValue = valueFactory.createValue("", PropertyType.BINARY);
} else {
throw new UnsupportedOperationException("Unsupported value type " + value.getClass());
}
return jcrValue;
}
private void createBinary(InputStream value, String suffix) throws RepositoryException {
// this is called inside the node of the child
String path = org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(currentNodeStack.peek().getPath(npResolver)) + suffix;
try {
// write binary directly (not during finish)
packageAssembler.addEntry(path, value);
} catch (IOException e) {
throw new RepositoryException(e);
}
}
@Override
public void createFileAndResourceNode(String name, InputStream data, String mimeType, long lastModified) throws RepositoryException {
this.createNode(name, JcrConstants.NT_FILE, null);
createBinary(data, ""); // binary must be created with the name of the nt:file root node
this.createNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE, null);
final Date date;
// ensure sensible last modification date
if (lastModified >= 0) {
date = new Date(lastModified);
} else {
date = new Date();
}
this.createProperty(JcrConstants.JCR_MIMETYPE, mimeType);
this.createProperty(JcrConstants.JCR_LASTMODIFIED, date);
// the data property does not need to be added to the enhanced docview as already derived from the binary file
}
@Override
public boolean switchCurrentNode(String subPath, String newNodeType) {
throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
}
@Override
public void createUser(String name, String password, Map<String, Object> extraProperties) {
throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
}
@Override
public void createGroup(String name, String[] members, Map<String, Object> extraProperties) {
throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
}
@Override
public void createAce(String principal, String[] grantedPrivileges, String[] deniedPrivileges, String order) {
throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
}
@Override
public void createAce(String principalId, String[] grantedPrivilegeNames, String[] deniedPrivilegeNames,
String order, Map<String, Value> restrictions, Map<String, Value[]> mvRestrictions,
Set<String> removedRestrictionNames) {
throw new UnsupportedOperationException(ACL_NOT_SUPPORTED_MSG);
}
}