blob: bff161e3bf1679b03aad1a1f05f657a133172093 [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.jackrabbit.vault.util;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.jcr.Credentials;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.NamespaceException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import org.apache.jackrabbit.vault.fs.api.ProgressTrackerListener;
import org.apache.jackrabbit.vault.fs.api.RepositoryAddress;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.io.AutoSave;
import org.apache.jackrabbit.vault.fs.spi.ProgressTracker;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/**
* Repository Copier that copies content from a source to a destination repository.
*/
public class RepositoryCopier {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(RepositoryCopier.class);
protected ProgressTrackerListener tracker;
private transient int numNodes = 0;
private transient int totalNodes = 0;
private transient long totalSize = 0;
private transient long currentSize = 0;
private transient long start = 0;
private transient String lastKnownGood;
private transient String currentPath;
private transient String cqLastModified;
private volatile boolean abort;
/** actual settings used by the copy process */
private int batchSize = 1024;
private long throttle = 0;
private transient String resumeFrom;
private WorkspaceFilter srcFilter;
private Map<String, String> prefixMapping = new HashMap<>();
private boolean onlyNewer;
private boolean update;
private boolean noOrdering;
private CredentialsProvider credentialsProvider;
public void setTracker(ProgressTrackerListener tracker) {
this.tracker = tracker;
}
public int getBatchSize() {
return batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public long getThrottle() {
return throttle;
}
public void setThrottle(long throttle) {
this.throttle = throttle;
}
public void setSourceFilter(WorkspaceFilter srcFilter) {
this.srcFilter = srcFilter;
}
public void setOnlyNewer(boolean onlyNewer) {
this.onlyNewer = onlyNewer;
}
public void setUpdate(boolean update) {
this.update = update;
}
public boolean isNoOrdering() {
return noOrdering;
}
public void setNoOrdering(boolean noOrdering) {
this.noOrdering = noOrdering;
}
public boolean isOnlyNewer() {
return onlyNewer;
}
public boolean isUpdate() {
return update;
}
public WorkspaceFilter getSrcFilter() {
return srcFilter;
}
public String getResumeFrom() {
return resumeFrom;
}
public void setResumeFrom(String resumeFrom) {
this.resumeFrom = resumeFrom;
}
public String getLastKnownGood() {
return lastKnownGood;
}
public String getCurrentPath() {
return currentPath;
}
public int getCurrentNumNodes() {
return numNodes;
}
public int getTotalNodes() {
return totalNodes;
}
public long getTotalSize() {
return totalSize;
}
public long getCurrentSize() {
return currentSize;
}
public void abort() {
abort = true;
}
public void copy(RepositoryAddress src, RepositoryAddress dst, boolean recursive) throws RepositoryException {
track("", "Copy %s to %s (%srecursive)", src, dst, recursive ? "" : "non-");
Session srcSession = null;
Session dstSession = null;
try {
RepositoryProvider repProvider = new RepositoryProvider();
Repository srcRepo;
try {
srcRepo = repProvider.getRepository(src);
} catch (RepositoryException e) {
throw new RepositoryException("Error while retrieving source repository " + src, e);
}
Repository dstRepo;
try {
dstRepo = repProvider.getRepository(dst);
} catch (RepositoryException e) {
throw new RepositoryException("Error while retrieving destination repository " + dst, e);
}
try {
Credentials srcCreds = src.getCredentials();
if (srcCreds == null && credentialsProvider != null) {
srcCreds = credentialsProvider.getCredentials(src);
}
srcSession = srcRepo.login(srcCreds, src.getWorkspace());
} catch (RepositoryException e) {
throw new RepositoryException("Could not log into source repository " + src, e);
}
try {
Credentials dstCreds = dst.getCredentials();
if (dstCreds == null && credentialsProvider != null) {
dstCreds = credentialsProvider.getCredentials(dst);
}
dstSession = dstRepo.login(dstCreds, dst.getWorkspace());
} catch (RepositoryException e) {
throw new RepositoryException("Could not log into destination repository " + dst, e);
}
copy(srcSession, src.getPath(), dstSession, dst.getPath(), recursive);
} finally {
if (srcSession != null) {
srcSession.logout();
}
if (dstSession != null) {
dstSession.logout();
}
}
}
public void copy(Session srcSession, String srcPath, Session dstSession, String dstPath, boolean recursive) throws RepositoryException {
if (srcSession == null || dstSession == null) {
throw new IllegalArgumentException("no src or dst session provided");
}
// get root nodes
String dstParent = Text.getRelativeParent(dstPath, 1);
String dstName = checkNameSpace(Text.getName(dstPath), srcSession, dstSession);
Node srcRoot;
try {
srcRoot = srcSession.getNode(srcPath);
} catch (RepositoryException e) {
throw new RepositoryException("Error while retrieving source node " + srcPath, e);
}
Node dstRoot;
try {
dstRoot = dstSession.getNode(dstParent);
} catch (RepositoryException e) {
throw new RepositoryException("Error while retrieving destination parent node " + dstParent, e);
}
// check if the cq namespace exists
try {
cqLastModified = srcSession.getNamespacePrefix("http://www.day.com/jcr/cq/1.0") + ":lastModified";
} catch (RepositoryException e) {
// ignore
log.debug("Haven't found cq namespace", e);
}
numNodes = 0;
totalNodes = 0;
currentSize = 0;
totalSize = 0;
start = System.currentTimeMillis();
AutoSave autoSave = new AutoSave();
autoSave.setThreshold(getBatchSize());
autoSave.setTracker(new ProgressTracker(tracker));
copy(autoSave, srcRoot, dstRoot, dstName, recursive);
if (numNodes > 0) {
track("", "Saving %d nodes...", numNodes);
autoSave.save(dstSession, false);
track("", "Done.");
}
long end = System.currentTimeMillis();
track("", "Copy completed. %d nodes in %dms. %d bytes", totalNodes, end-start, totalSize);
}
private void copy(AutoSave autoSave, Node src, Node dstParent, String dstName, boolean recursive)
throws RepositoryException {
if (abort) {
return;
}
String path = src.getPath();
currentPath = path;
String dstPath = dstParent.getPath() + "/" + dstName;
if (srcFilter != null && !srcFilter.contains(path)) {
track(path, "------ I");
return;
}
boolean skip = false;
if (resumeFrom != null) {
if (path.equals(resumeFrom)) {
// found last node, resuming
resumeFrom = null;
} else {
skip = true;
}
}
// check for special node that need sysview import handling
boolean useSysView = src.getDefinition().isProtected();
Node dst;
boolean isNew = false;
boolean overwrite = update;
if (dstParent.hasNode(dstName)) {
dst = dstParent.getNode(dstName);
if (skip) {
track(path, "------ S");
} else if (overwrite) {
if (onlyNewer && dstName.equals("jcr:content")) {
if (isNewer(src, dst)) {
track(dstPath, "%06d U", ++totalNodes);
} else {
overwrite = false;
recursive = false;
track(dstPath, "%06d -", ++totalNodes);
}
} else {
track(dstPath, "%06d U", ++totalNodes);
}
if (useSysView) {
dst = sysCopy(src, dstParent, dstName);
}
} else {
track(dstPath, "%06d -", ++totalNodes);
}
} else {
try {
if (skip) {
track(path, "------ S");
dst = null;
} else if (useSysView) {
dst = sysCopy(src, dstParent, dstName);
} else {
dst = dstParent.addNode(dstName, src.getPrimaryNodeType().getName());
}
track(dstPath, "%06d A", ++totalNodes);
isNew = true;
} catch (RepositoryException e) {
if (log.isDebugEnabled()) {
log.debug("Error while adding node {} (ignored)", dstPath, e);
} else {
log.warn("Error while adding node {} (ignored): {}", dstPath, e.getMessage());
}
return;
}
}
if (useSysView) {
if (!skip) {
// track changes
trackTree(dst, isNew);
}
} else {
Set<String> names = new HashSet<String>();
if (!skip && (overwrite || isNew)) {
if (!isNew) {
for (NodeType nt: dst.getMixinNodeTypes()) {
names.add(nt.getName());
}
// add mixins
for (NodeType nt: src.getMixinNodeTypes()) {
String mixName = checkNameSpace(nt.getName(), src.getSession(), dst.getSession());
if (!names.remove(mixName)) {
dst.addMixin(nt.getName());
}
}
// handle removed mixins
for (String mix: names) {
dst.removeMixin(mix);
}
} else {
// add mixins
for (NodeType nt: src.getMixinNodeTypes()) {
dst.addMixin(checkNameSpace(nt.getName(), src.getSession(), dst.getSession()));
}
}
// add properties
names.clear();
if (!isNew) {
PropertyIterator iter = dst.getProperties();
while (iter.hasNext()) {
names.add(checkNameSpace(iter.nextProperty().getName(), src.getSession(), dst.getSession()));
}
}
PropertyIterator iter = src.getProperties();
while (iter.hasNext()) {
Property p = iter.nextProperty();
String pName = checkNameSpace(p.getName(), src.getSession(), dst.getSession());
names.remove(pName);
// ignore protected
if (p.getDefinition().isProtected()) {
continue;
}
// remove destination property to avoid type clashes
if (dst.hasProperty(pName)) {
dst.getProperty(pName).remove();
}
if (p.getDefinition().isMultiple()) {
Value[] vs = p.getValues();
dst.setProperty(pName, vs);
for (long s: p.getLengths()) {
totalSize+=s;
currentSize+=s;
}
} else {
Value v = p.getValue();
dst.setProperty(pName, v);
long s= p.getLength();
totalSize+=s;
currentSize+=s;
}
}
// remove obsolete properties
for (String pName: names) {
try {
// ignore protected. should not happen, unless the primary node type changes.
Property dstP = dst.getProperty(pName);
if (dstP.getDefinition().isProtected()) {
continue;
}
dstP.remove();
} catch (RepositoryException e) {
// ignore
}
}
}
// descend
if (recursive && dst != null) {
names.clear();
if (overwrite && !isNew) {
NodeIterator niter = dst.getNodes();
while (niter.hasNext()) {
names.add(checkNameSpace(niter.nextNode().getName(), src.getSession(), dst.getSession()));
}
}
NodeIterator niter = src.getNodes();
while (niter.hasNext()) {
Node child = niter.nextNode();
String cName = checkNameSpace(child.getName(), src.getSession(), dst.getSession());
names.remove(cName);
copy(autoSave, child, dst, cName, true);
}
if (resumeFrom == null) {
// check if we need to order
if (overwrite && !isNew && !noOrdering && src.getPrimaryNodeType().hasOrderableChildNodes()) {
niter = src.getNodes();
while (niter.hasNext()) {
Node child = niter.nextNode();
String name = child.getName();
if (dst.hasNode(name)) {
dst.orderBefore(name, null);
}
}
}
// remove obsolete child nodes
for (String name: names) {
try {
Node cNode = dst.getNode(name);
track(cNode.getPath(), "%06d D", ++totalNodes);
cNode.remove();
} catch (RepositoryException e) {
// ignore
}
}
}
}
}
if (!skip) {
numNodes++;
autoSave.modified(1);
}
// check for save
if (autoSave.needsSave()) {
track("", "Intermediate saving %d nodes (%d kB)...", numNodes, currentSize/1000);
long now = System.currentTimeMillis();
autoSave.save(dst.getSession(), true);
long end = System.currentTimeMillis();
track("", "Done in %d ms. Total time: %d, total nodes %d, %d kB", end-now, end-start, totalNodes, totalSize/1000);
lastKnownGood = currentPath;
numNodes = 0;
currentSize = 0;
if (throttle > 0) {
track("", "Throttling enabled. Waiting %d second%s...", throttle, throttle == 1 ? "" : "s");
try {
Thread.sleep(throttle * 1000);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting", e);
Thread.currentThread().interrupt();
}
}
}
}
private Node sysCopy(Node src, Node dstParent, String dstName) throws RepositoryException {
try {
ContentHandler handler = dstParent.getSession().getImportContentHandler(dstParent.getPath(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
src.getSession().exportSystemView(src.getPath(), handler, true, false);
return dstParent.getNode(dstName);
} catch (SAXException e) {
throw new RepositoryException("Unable to perform sysview copy", e);
}
}
private void trackTree(Node node, boolean isNew) throws RepositoryException {
NodeIterator iter = node.getNodes();
while (iter.hasNext()) {
Node child = iter.nextNode();
if (isNew) {
track(child.getPath(), "%06d A", ++totalNodes);
} else {
track(child.getPath(), "%06d U", ++totalNodes);
}
trackTree(child, isNew);
}
}
/**
* Checks if {@code src} node is newer than {@code dst} node.
* this only applies if the nodes have either a "jcr:lastModified" or
* "cq:lastModified" property.
*
* @param src source node
* @param dst destination node
* @return {@code true} if src is newer than dst node or if the
* nodes could not be compared
*/
private boolean isNewer(Node src, Node dst) {
try {
Calendar srcDate = null;
Calendar dstDate = null;
if (cqLastModified != null && src.hasProperty(cqLastModified) && dst.hasProperty(cqLastModified)) {
srcDate = src.getProperty(cqLastModified).getDate();
dstDate = dst.getProperty(cqLastModified).getDate();
} else if (src.hasProperty(JcrConstants.JCR_LASTMODIFIED) && dst.hasProperty(JcrConstants.JCR_LASTMODIFIED)) {
srcDate = src.getProperty(JcrConstants.JCR_LASTMODIFIED).getDate();
dstDate = dst.getProperty(JcrConstants.JCR_LASTMODIFIED).getDate();
}
return srcDate == null || dstDate == null || srcDate.after(dstDate);
} catch (RepositoryException e) {
log.error("Unable to compare dates: {}", e.toString());
return true;
}
}
private String checkNameSpace(String name, Session srcSession, Session dstSession) {
try {
int idx = name.indexOf(':');
if (idx > 0) {
String prefix = name.substring(0, idx);
String mapped = prefixMapping.get(prefix);
if (mapped == null) {
String uri = srcSession.getNamespaceURI(prefix);
try {
mapped = dstSession.getNamespacePrefix(uri);
} catch (NamespaceException e) {
mapped = prefix;
int i=0;
while (i>=0) {
try {
dstSession.getWorkspace().getNamespaceRegistry().registerNamespace(mapped, uri);
i=-1;
} catch (NamespaceException e1) {
mapped = prefix + i++;
}
}
}
prefixMapping.put(prefix, mapped);
}
if (mapped.equals(prefix)) {
return name;
} else {
return mapped + name.substring(idx);
}
}
} catch (RepositoryException e) {
log.error("Error processing namespace for {}: {}", name, e.toString());
}
return name;
}
private void track(String path, String fmt, Object ... args) {
if (tracker != null) {
tracker.onMessage(ProgressTrackerListener.Mode.TEXT, String.format(fmt, args), path);
}
}
public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
}
public CredentialsProvider getCredentialsProvider() {
return credentialsProvider;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (abort ? 1231 : 1237);
result = prime * result + batchSize;
result = prime * result + (noOrdering ? 1231 : 1237);
result = prime * result + (onlyNewer ? 1231 : 1237);
result = prime * result + ((prefixMapping == null) ? 0 : prefixMapping.hashCode());
result = prime * result + ((resumeFrom == null) ? 0 : resumeFrom.hashCode());
result = prime * result + ((srcFilter == null) ? 0 : srcFilter.hashCode());
result = prime * result + (int) (throttle ^ (throttle >>> 32));
result = prime * result + ((tracker == null) ? 0 : tracker.hashCode());
result = prime * result + (update ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RepositoryCopier other = (RepositoryCopier) obj;
if (abort != other.abort)
return false;
if (batchSize != other.batchSize)
return false;
if (noOrdering != other.noOrdering)
return false;
if (onlyNewer != other.onlyNewer)
return false;
if (prefixMapping == null) {
if (other.prefixMapping != null)
return false;
} else if (!prefixMapping.equals(other.prefixMapping))
return false;
if (resumeFrom == null) {
if (other.resumeFrom != null)
return false;
} else if (!resumeFrom.equals(other.resumeFrom))
return false;
if (srcFilter == null) {
if (other.srcFilter != null)
return false;
} else if (!srcFilter.equals(other.srcFilter))
return false;
if (throttle != other.throttle)
return false;
if (tracker == null) {
if (other.tracker != null)
return false;
} else if (!tracker.equals(other.tracker))
return false;
if (update != other.update)
return false;
return true;
}
}