2 * Copyright (C) 2007-2012 Argeo GmbH
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org
.argeo
.slc
.repo
;
18 import java
.io
.InputStream
;
19 import java
.util
.Arrays
;
20 import java
.util
.Calendar
;
21 import java
.util
.GregorianCalendar
;
22 import java
.util
.HashMap
;
23 import java
.util
.List
;
25 import java
.util
.TimeZone
;
27 import javax
.jcr
.Binary
;
28 import javax
.jcr
.Credentials
;
29 import javax
.jcr
.NoSuchWorkspaceException
;
30 import javax
.jcr
.Node
;
31 import javax
.jcr
.NodeIterator
;
32 import javax
.jcr
.Property
;
33 import javax
.jcr
.PropertyIterator
;
34 import javax
.jcr
.PropertyType
;
35 import javax
.jcr
.Repository
;
36 import javax
.jcr
.RepositoryException
;
37 import javax
.jcr
.RepositoryFactory
;
38 import javax
.jcr
.Session
;
39 import javax
.jcr
.SimpleCredentials
;
40 import javax
.jcr
.nodetype
.NodeType
;
41 import javax
.jcr
.query
.Query
;
42 import javax
.jcr
.query
.QueryResult
;
44 import org
.apache
.commons
.io
.IOUtils
;
45 import org
.apache
.commons
.logging
.Log
;
46 import org
.apache
.commons
.logging
.LogFactory
;
47 import org
.argeo
.jcr
.JcrMonitor
;
48 import org
.argeo
.jcr
.JcrUtils
;
49 import org
.argeo
.node
.NodeUtils
;
50 import org
.argeo
.slc
.SlcException
;
51 import org
.xml
.sax
.SAXException
;
54 * Synchronise workspaces from a remote software repository to the local
55 * repository (Synchronisation in the other direction does not work).
57 * Workspaces are retrieved by name given a map that links the source with a
58 * target name. If a target workspace does not exist, it is created. Otherwise
59 * we copy the content of the source workspace into the target one.
61 public class RepoSync
implements Runnable
{
62 private final static Log log
= LogFactory
.getLog(RepoSync
.class);
64 // Centralizes definition of workspaces that must be ignored by the sync.
65 private final static List
<String
> IGNORED_WKSP_LIST
= Arrays
.asList("security", "localrepo");
67 private final Calendar zero
;
68 private Session sourceDefaultSession
= null;
69 private Session targetDefaultSession
= null;
71 private Repository sourceRepository
;
72 private Credentials sourceCredentials
;
73 private Repository targetRepository
;
74 private Credentials targetCredentials
;
76 // if Repository and Credentials objects are not explicitly set
77 private String sourceRepoUri
;
78 private String sourceUsername
;
79 private char[] sourcePassword
;
80 private String targetRepoUri
;
81 private String targetUsername
;
82 private char[] targetPassword
;
84 private RepositoryFactory repositoryFactory
;
86 private JcrMonitor monitor
;
87 private Map
<String
, String
> workspaceMap
;
90 private Boolean filesOnly
= false;
93 zero
= new GregorianCalendar(TimeZone
.getTimeZone("UTC"));
94 zero
.setTimeInMillis(0);
99 * Shortcut to instantiate a RepoSync with already known repositories and
102 * @param sourceRepository
103 * @param sourceCredentials
104 * @param targetRepository
105 * @param targetCredentials
107 public RepoSync(Repository sourceRepository
, Credentials sourceCredentials
, Repository targetRepository
,
108 Credentials targetCredentials
) {
110 this.sourceRepository
= sourceRepository
;
111 this.sourceCredentials
= sourceCredentials
;
112 this.targetRepository
= targetRepository
;
113 this.targetCredentials
= targetCredentials
;
118 long begin
= System
.currentTimeMillis();
121 if (sourceRepository
== null)
122 sourceRepository
= NodeUtils
.getRepositoryByUri(repositoryFactory
, sourceRepoUri
);
123 if (sourceCredentials
== null && sourceUsername
!= null)
124 sourceCredentials
= new SimpleCredentials(sourceUsername
, sourcePassword
);
125 // FIXME make it more generic
126 sourceDefaultSession
= sourceRepository
.login(sourceCredentials
, RepoConstants
.DEFAULT_DEFAULT_WORKSPACE
);
128 if (targetRepository
== null)
129 targetRepository
= NodeUtils
.getRepositoryByUri(repositoryFactory
, targetRepoUri
);
130 if (targetCredentials
== null && targetUsername
!= null)
131 targetCredentials
= new SimpleCredentials(targetUsername
, targetPassword
);
132 targetDefaultSession
= targetRepository
.login(targetCredentials
);
134 Map
<String
, Exception
> errors
= new HashMap
<String
, Exception
>();
135 for (String sourceWorkspaceName
: sourceDefaultSession
.getWorkspace().getAccessibleWorkspaceNames()) {
136 if (monitor
!= null && monitor
.isCanceled())
139 if (workspaceMap
!= null && !workspaceMap
.containsKey(sourceWorkspaceName
))
141 if (IGNORED_WKSP_LIST
.contains(sourceWorkspaceName
))
144 Session sourceSession
= null;
145 Session targetSession
= null;
146 String targetWorkspaceName
= workspaceMap
.get(sourceWorkspaceName
);
149 targetSession
= targetRepository
.login(targetCredentials
, targetWorkspaceName
);
150 } catch (NoSuchWorkspaceException e
) {
151 targetDefaultSession
.getWorkspace().createWorkspace(targetWorkspaceName
);
152 targetSession
= targetRepository
.login(targetCredentials
, targetWorkspaceName
);
154 sourceSession
= sourceRepository
.login(sourceCredentials
, sourceWorkspaceName
);
155 syncWorkspace(sourceSession
, targetSession
);
156 } catch (Exception e
) {
157 errors
.put("Could not sync workspace " + sourceWorkspaceName
, e
);
158 if (log
.isErrorEnabled())
162 JcrUtils
.logoutQuietly(sourceSession
);
163 JcrUtils
.logoutQuietly(targetSession
);
167 if (monitor
!= null && monitor
.isCanceled())
168 log
.info("Sync has been canceled by user");
170 long duration
= (System
.currentTimeMillis() - begin
) / 1000;// s
171 log
.info("Sync " + sourceRepoUri
+ " to " + targetRepoUri
+ " in " + (duration
/ 60)
173 + "min " + (duration
% 60) + "s");
175 if (errors
.size() > 0) {
176 throw new SlcException("Sync failed " + errors
);
178 } catch (RepositoryException e
) {
179 throw new SlcException("Cannot sync " + sourceRepoUri
+ " to " + targetRepoUri
, e
);
181 JcrUtils
.logoutQuietly(sourceDefaultSession
);
182 JcrUtils
.logoutQuietly(targetDefaultSession
);
186 private long getNodesNumber(Session session
) {
187 if (IGNORED_WKSP_LIST
.contains(session
.getWorkspace().getName()))
190 Query countQuery
= session
.getWorkspace().getQueryManager().createQuery(
191 "select file from [" + (true ? NodeType
.NT_FILE
: NodeType
.NT_BASE
) + "] as file", Query
.JCR_SQL2
);
193 QueryResult result
= countQuery
.execute();
194 Long expectedCount
= result
.getNodes().getSize();
195 return expectedCount
;
196 } catch (RepositoryException e
) {
197 throw new SlcException("Unexpected error while computing " + "the size of the fetch for workspace "
198 + session
.getWorkspace().getName(), e
);
202 protected void syncWorkspace(Session sourceSession
, Session targetSession
) {
203 if (monitor
!= null) {
204 monitor
.beginTask("Computing fetch size...", -1);
205 Long totalAmount
= getNodesNumber(sourceSession
);
206 monitor
.beginTask("Fetch", totalAmount
.intValue());
210 String msg
= "Synchronizing workspace: " + sourceSession
.getWorkspace().getName();
212 monitor
.setTaskName(msg
);
213 if (log
.isDebugEnabled())
216 for (NodeIterator it
= sourceSession
.getRootNode().getNodes(); it
.hasNext();) {
217 Node node
= it
.nextNode();
218 if (node
.getName().contains(":"))
220 if (node
.getName().equals("download"))
222 if (!node
.isNodeType(NodeType
.NT_HIERARCHY_NODE
))
224 syncNode(node
, targetSession
);
227 // JcrUtils.copyFiles(sourceSession.getRootNode(), targetSession.getRootNode(),
230 // for (NodeIterator it = sourceSession.getRootNode().getNodes(); it.hasNext();)
232 // Node node = it.nextNode();
233 // if (node.getName().equals("jcr:system"))
235 // syncNode(node, targetSession);
238 if (log
.isDebugEnabled())
239 log
.debug("Synced " + sourceSession
.getWorkspace().getName());
240 } catch (Exception e
) {
242 throw new SlcException("Cannot sync " + sourceSession
.getWorkspace().getName() + " to "
243 + targetSession
.getWorkspace().getName(), e
);
247 /** factorizes monitor management */
248 private void updateMonitor(String msg
) {
249 updateMonitor(msg
, false);
252 protected void syncNode(Node sourceNode
, Session targetSession
) throws RepositoryException
, SAXException
{
255 if (targetSession
.itemExists(sourceNode
.getPath()))
256 targetNode
= targetSession
.getNode(sourceNode
.getPath());
258 targetNode
= JcrUtils
.mkdirs(targetSession
, sourceNode
.getPath(), NodeType
.NT_FOLDER
);
259 JcrUtils
.copyFiles(sourceNode
, targetNode
, true, monitor
, true);
262 // Boolean singleLevel = singleLevel(sourceNode);
264 if (monitor
!= null && monitor
.isCanceled()) {
265 updateMonitor("Fetched has been canceled, " + "process is terminating");
269 Node targetParentNode
= targetSession
.getNode(sourceNode
.getParent().getPath());
271 if (monitor
!= null && sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
))
272 monitor
.subTask("Process " + sourceNode
.getPath());
275 if (!targetSession
.itemExists(sourceNode
.getPath())) {
277 targetNode
= targetParentNode
.addNode(sourceNode
.getName(), sourceNode
.getPrimaryNodeType().getName());
280 targetNode
= targetSession
.getNode(sourceNode
.getPath());
281 if (!targetNode
.getPrimaryNodeType().getName().equals(sourceNode
.getPrimaryNodeType().getName()))
282 targetNode
.setPrimaryType(sourceNode
.getPrimaryNodeType().getName());
286 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
287 // contentHandler, false, singleLevel);
289 // if (singleLevel) {
290 // if (targetSession.hasPendingChanges()) {
292 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
295 // targetSession.save();
297 // // updateMonitor("Checked " + targetNode.getPath(), false);
301 // mixin and properties
302 for (NodeType nt
: sourceNode
.getMixinNodeTypes()) {
303 if (!targetNode
.isNodeType(nt
.getName()) && targetNode
.canAddMixin(nt
.getName()))
304 targetNode
.addMixin(nt
.getName());
306 copyProperties(sourceNode
, targetNode
);
309 NodeIterator ni
= sourceNode
.getNodes();
310 while (ni
!= null && ni
.hasNext()) {
311 Node sourceChild
= ni
.nextNode();
312 syncNode(sourceChild
, targetSession
);
315 copyTimestamps(sourceNode
, targetNode
);
317 if (sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
)) {
318 if (targetSession
.hasPendingChanges()) {
319 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
320 updateMonitor((isNew ?
"Added " : "Updated ") + targetNode
.getPath(), true);
322 targetSession
.save();
324 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
325 updateMonitor("Checked " + targetNode
.getPath(), false);
328 } catch (RepositoryException e
) {
329 throw new SlcException("Cannot sync source node " + sourceNode
, e
);
333 private void copyTimestamps(Node sourceNode
, Node targetNode
) throws RepositoryException
{
334 if (sourceNode
.getDefinition().isProtected())
336 if (targetNode
.getDefinition().isProtected())
338 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED
);
339 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED_BY
);
340 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED
);
341 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED_BY
);
344 private void copyTimestamp(Node sourceNode
, Node targetNode
, String property
) throws RepositoryException
{
345 if (sourceNode
.hasProperty(property
)) {
346 Property p
= sourceNode
.getProperty(property
);
347 if (p
.getDefinition().isProtected())
349 if (targetNode
.hasProperty(property
)
350 && targetNode
.getProperty(property
).getValue().equals(sourceNode
.getProperty(property
).getValue()))
352 targetNode
.setProperty(property
, sourceNode
.getProperty(property
).getValue());
356 private void copyProperties(Node sourceNode
, Node targetNode
) throws RepositoryException
{
357 properties
: for (PropertyIterator pi
= sourceNode
.getProperties(); pi
.hasNext();) {
358 Property p
= pi
.nextProperty();
359 if (p
.getDefinition().isProtected())
361 if (p
.getName().equals(Property
.JCR_CREATED
) || p
.getName().equals(Property
.JCR_CREATED_BY
)
362 || p
.getName().equals(Property
.JCR_LAST_MODIFIED
)
363 || p
.getName().equals(Property
.JCR_LAST_MODIFIED_BY
))
366 if (p
.getType() == PropertyType
.BINARY
) {
367 copyBinary(p
, targetNode
);
370 if (p
.isMultiple()) {
371 if (!targetNode
.hasProperty(p
.getName())
372 || !Arrays
.equals(targetNode
.getProperty(p
.getName()).getValues(), p
.getValues()))
373 targetNode
.setProperty(p
.getName(), p
.getValues());
375 if (!targetNode
.hasProperty(p
.getName())
376 || !targetNode
.getProperty(p
.getName()).getValue().equals(p
.getValue()))
377 targetNode
.setProperty(p
.getName(), p
.getValue());
383 private static void copyBinary(Property p
, Node targetNode
) throws RepositoryException
{
384 InputStream in
= null;
385 Binary sourceBinary
= null;
386 Binary targetBinary
= null;
388 sourceBinary
= p
.getBinary();
389 if (targetNode
.hasProperty(p
.getName()))
390 targetBinary
= targetNode
.getProperty(p
.getName()).getBinary();
392 // optim FIXME make it more configurable
393 if (targetBinary
!= null)
394 if (sourceBinary
.getSize() == targetBinary
.getSize()) {
395 if (log
.isTraceEnabled())
396 log
.trace("Skipped " + p
.getPath());
400 in
= sourceBinary
.getStream();
401 targetBinary
= targetNode
.getSession().getValueFactory().createBinary(in
);
402 targetNode
.setProperty(p
.getName(), targetBinary
);
403 } catch (Exception e
) {
404 throw new SlcException("Could not transfer " + p
, e
);
406 IOUtils
.closeQuietly(in
);
407 JcrUtils
.closeQuietly(sourceBinary
);
408 JcrUtils
.closeQuietly(targetBinary
);
412 /** factorizes monitor management */
413 private void updateMonitor(String msg
, Boolean doLog
) {
414 if (doLog
&& log
.isDebugEnabled())
416 if (monitor
!= null) {
418 monitor
.subTask(msg
);
422 // private void syncNode_old(Node sourceNode, Node targetParentNode)
423 // throws RepositoryException, SAXException {
425 // // enable cancelation of the current fetch process
426 // // fxme insure the repository stays in a stable state
427 // if (monitor != null && monitor.isCanceled()) {
428 // updateMonitor("Fetched has been canceled, "
429 // + "process is terminating");
433 // Boolean noRecurse = singleLevel(sourceNode);
434 // Calendar sourceLastModified = null;
435 // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
436 // sourceLastModified = sourceNode.getProperty(
437 // Property.JCR_LAST_MODIFIED).getDate();
440 // if (sourceNode.getDefinition().isProtected())
441 // log.warn(sourceNode + " is protected.");
443 // if (!targetParentNode.hasNode(sourceNode.getName())) {
444 // String msg = "Adding " + sourceNode.getPath();
445 // updateMonitor(msg);
446 // if (log.isDebugEnabled())
448 // ContentHandler contentHandler = targetParentNode
451 // .getImportContentHandler(targetParentNode.getPath(),
452 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
453 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
454 // contentHandler, false, noRecurse);
456 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
457 // if (sourceLastModified != null) {
458 // Calendar targetLastModified = null;
459 // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
460 // targetLastModified = targetNode.getProperty(
461 // Property.JCR_LAST_MODIFIED).getDate();
464 // if (targetLastModified == null
465 // || targetLastModified.before(sourceLastModified)) {
466 // String msg = "Updating " + targetNode.getPath();
467 // updateMonitor(msg);
468 // if (log.isDebugEnabled())
470 // ContentHandler contentHandler = targetParentNode
473 // .getImportContentHandler(
474 // targetParentNode.getPath(),
475 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
476 // sourceNode.getSession().exportSystemView(
477 // sourceNode.getPath(), contentHandler, false,
480 // String msg = "Skipped up to date " + targetNode.getPath();
481 // updateMonitor(msg);
482 // if (log.isDebugEnabled())
491 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
492 // if (sourceLastModified != null) {
493 // Calendar zero = new GregorianCalendar();
494 // zero.setTimeInMillis(0);
495 // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
496 // targetNode.getSession().save();
499 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
500 // syncNode_old(it.nextNode(), targetNode);
503 // if (sourceLastModified != null) {
504 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
505 // sourceLastModified);
506 // targetNode.getSession().save();
511 protected Boolean
singleLevel(Node sourceNode
) throws RepositoryException
{
512 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
518 * Synchronises only one workspace, retrieved by name without changing its name.
520 public void setSourceWksp(String sourceWksp
) {
521 if (sourceWksp
!= null && !sourceWksp
.trim().equals("")) {
522 Map
<String
, String
> map
= new HashMap
<String
, String
>();
523 map
.put(sourceWksp
, sourceWksp
);
529 * Synchronises a map of workspaces that will be retrieved by name. If the
530 * target name is not defined (eg null or an empty string) for a given source
531 * workspace, we use the source name as target name.
533 public void setWkspMap(Map
<String
, String
> workspaceMap
) {
534 // clean the list to ease later use
535 this.workspaceMap
= new HashMap
<String
, String
>();
536 if (workspaceMap
!= null) {
537 workspaceNames
: for (String srcName
: workspaceMap
.keySet()) {
538 String targetName
= workspaceMap
.get(srcName
);
541 if (srcName
.trim().equals(""))
542 continue workspaceNames
;
543 if (targetName
== null || "".equals(targetName
.trim()))
544 targetName
= srcName
;
545 this.workspaceMap
.put(srcName
, targetName
);
548 // clean the map to ease later use
549 if (this.workspaceMap
.size() == 0)
550 this.workspaceMap
= null;
553 public void setMonitor(JcrMonitor monitor
) {
554 this.monitor
= monitor
;
557 public void setRepositoryFactory(RepositoryFactory repositoryFactory
) {
558 this.repositoryFactory
= repositoryFactory
;
561 public void setSourceRepoUri(String sourceRepoUri
) {
562 this.sourceRepoUri
= sourceRepoUri
;
565 public void setSourceUsername(String sourceUsername
) {
566 this.sourceUsername
= sourceUsername
;
569 public void setSourcePassword(char[] sourcePassword
) {
570 this.sourcePassword
= sourcePassword
;
573 public void setTargetRepoUri(String targetRepoUri
) {
574 this.targetRepoUri
= targetRepoUri
;
577 public void setTargetUsername(String targetUsername
) {
578 this.targetUsername
= targetUsername
;
581 public void setTargetPassword(char[] targetPassword
) {
582 this.targetPassword
= targetPassword
;
585 public void setSourceRepository(Repository sourceRepository
) {
586 this.sourceRepository
= sourceRepository
;
589 public void setSourceCredentials(Credentials sourceCredentials
) {
590 this.sourceCredentials
= sourceCredentials
;
593 public void setTargetRepository(Repository targetRepository
) {
594 this.targetRepository
= targetRepository
;
597 public void setTargetCredentials(Credentials targetCredentials
) {
598 this.targetCredentials
= targetCredentials
;
601 public void setFilesOnly(Boolean filesOnly
) {
602 this.filesOnly
= filesOnly
;