| /* |
| * 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; |
| } |
| |
| } |