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
.ArgeoMonitor
;
48 import org
.argeo
.jcr
.ArgeoJcrUtils
;
49 import org
.argeo
.jcr
.JcrUtils
;
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(
66 "security", "localrepo");
68 private final Calendar zero
;
69 private Session sourceDefaultSession
= null;
70 private Session targetDefaultSession
= null;
72 private Repository sourceRepository
;
73 private Credentials sourceCredentials
;
74 private Repository targetRepository
;
75 private Credentials targetCredentials
;
77 // if Repository and Credentials objects are not explicitly set
78 private String sourceRepoUri
;
79 private String sourceUsername
;
80 private char[] sourcePassword
;
81 private String targetRepoUri
;
82 private String targetUsername
;
83 private char[] targetPassword
;
85 private RepositoryFactory repositoryFactory
;
87 private ArgeoMonitor monitor
;
88 private Map
<String
, String
> workspaceMap
;
91 private Boolean filesOnly
= false;
94 zero
= new GregorianCalendar(TimeZone
.getTimeZone("UTC"));
95 zero
.setTimeInMillis(0);
100 * Shortcut to instantiate a RepoSync with already known repositories and
103 * @param sourceRepository
104 * @param sourceCredentials
105 * @param targetRepository
106 * @param targetCredentials
108 public RepoSync(Repository sourceRepository
, Credentials sourceCredentials
,
109 Repository targetRepository
, Credentials targetCredentials
) {
111 this.sourceRepository
= sourceRepository
;
112 this.sourceCredentials
= sourceCredentials
;
113 this.targetRepository
= targetRepository
;
114 this.targetCredentials
= targetCredentials
;
119 long begin
= System
.currentTimeMillis();
122 if (sourceRepository
== null)
123 sourceRepository
= ArgeoJcrUtils
.getRepositoryByUri(
124 repositoryFactory
, sourceRepoUri
);
125 if (sourceCredentials
== null && sourceUsername
!= null)
126 sourceCredentials
= new SimpleCredentials(sourceUsername
,
128 sourceDefaultSession
= sourceRepository
.login(sourceCredentials
);
130 if (targetRepository
== null)
131 targetRepository
= ArgeoJcrUtils
.getRepositoryByUri(
132 repositoryFactory
, targetRepoUri
);
133 if (targetCredentials
== null && targetUsername
!= null)
134 targetCredentials
= new SimpleCredentials(targetUsername
,
136 targetDefaultSession
= targetRepository
.login(targetCredentials
);
138 Map
<String
, Exception
> errors
= new HashMap
<String
, Exception
>();
139 for (String sourceWorkspaceName
: sourceDefaultSession
140 .getWorkspace().getAccessibleWorkspaceNames()) {
141 if (monitor
!= null && monitor
.isCanceled())
144 if (workspaceMap
!= null
145 && !workspaceMap
.containsKey(sourceWorkspaceName
))
147 if (IGNORED_WKSP_LIST
.contains(sourceWorkspaceName
))
150 Session sourceSession
= null;
151 Session targetSession
= null;
152 String targetWorkspaceName
= workspaceMap
153 .get(sourceWorkspaceName
);
156 targetSession
= targetRepository
.login(
157 targetCredentials
, targetWorkspaceName
);
158 } catch (NoSuchWorkspaceException e
) {
159 targetDefaultSession
.getWorkspace().createWorkspace(
160 targetWorkspaceName
);
161 targetSession
= targetRepository
.login(
162 targetCredentials
, targetWorkspaceName
);
164 sourceSession
= sourceRepository
.login(sourceCredentials
,
165 sourceWorkspaceName
);
166 syncWorkspace(sourceSession
, targetSession
);
167 } catch (Exception e
) {
168 errors
.put("Could not sync workspace "
169 + sourceWorkspaceName
, e
);
170 if (log
.isErrorEnabled())
174 JcrUtils
.logoutQuietly(sourceSession
);
175 JcrUtils
.logoutQuietly(targetSession
);
179 if (monitor
!= null && monitor
.isCanceled())
180 log
.info("Sync has been canceled by user");
182 long duration
= (System
.currentTimeMillis() - begin
) / 1000;// s
183 log
.info("Sync " + sourceRepoUri
+ " to " + targetRepoUri
+ " in "
186 + "min " + (duration
% 60) + "s");
188 if (errors
.size() > 0) {
189 throw new SlcException("Sync failed " + errors
);
191 } catch (RepositoryException e
) {
192 throw new SlcException("Cannot sync " + sourceRepoUri
+ " to "
195 JcrUtils
.logoutQuietly(sourceDefaultSession
);
196 JcrUtils
.logoutQuietly(targetDefaultSession
);
200 private long getNodesNumber(Session session
) {
201 if (IGNORED_WKSP_LIST
.contains(session
.getWorkspace().getName()))
204 Query countQuery
= session
209 + (true ?
"nt:file" : "nt:base")
210 + "] as file", Query
.JCR_SQL2
);
211 QueryResult result
= countQuery
.execute();
212 Long expectedCount
= result
.getNodes().getSize();
213 return expectedCount
;
214 } catch (RepositoryException e
) {
215 throw new SlcException("Unexpected error while computing "
216 + "the size of the fetch for workspace "
217 + session
.getWorkspace().getName(), e
);
221 protected void syncWorkspace(Session sourceSession
, Session targetSession
) {
222 if (monitor
!= null) {
223 monitor
.beginTask("Computing fetch size...", -1);
224 Long totalAmount
= getNodesNumber(sourceSession
);
225 monitor
.beginTask("Fetch", totalAmount
.intValue());
229 String msg
= "Synchronizing workspace: "
230 + sourceSession
.getWorkspace().getName();
232 monitor
.setTaskName(msg
);
233 if (log
.isDebugEnabled())
237 JcrUtils
.copyFiles(sourceSession
.getRootNode(),
238 targetSession
.getRootNode(), true, monitor
);
240 for (NodeIterator it
= sourceSession
.getRootNode().getNodes(); it
242 Node node
= it
.nextNode();
243 if (node
.getName().equals("jcr:system"))
245 syncNode(node
, targetSession
);
248 if (log
.isDebugEnabled())
249 log
.debug("Synced " + sourceSession
.getWorkspace().getName());
250 } catch (Exception e
) {
252 throw new SlcException("Cannot sync "
253 + sourceSession
.getWorkspace().getName() + " to "
254 + targetSession
.getWorkspace().getName(), e
);
258 /** factorizes monitor management */
259 private void updateMonitor(String msg
) {
260 updateMonitor(msg
, false);
263 protected void syncNode(Node sourceNode
, Session targetSession
)
264 throws RepositoryException
, SAXException
{
265 // Boolean singleLevel = singleLevel(sourceNode);
267 if (monitor
!= null && monitor
.isCanceled()) {
268 updateMonitor("Fetched has been canceled, "
269 + "process is terminating");
273 Node targetParentNode
= targetSession
.getNode(sourceNode
274 .getParent().getPath());
277 && sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
))
278 monitor
.subTask("Process " + sourceNode
.getPath());
281 if (!targetSession
.itemExists(sourceNode
.getPath())) {
283 targetNode
= targetParentNode
.addNode(sourceNode
.getName(),
284 sourceNode
.getPrimaryNodeType().getName());
287 targetNode
= targetSession
.getNode(sourceNode
.getPath());
288 if (!targetNode
.getPrimaryNodeType().getName()
289 .equals(sourceNode
.getPrimaryNodeType().getName()))
290 targetNode
.setPrimaryType(sourceNode
.getPrimaryNodeType()
295 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
296 // contentHandler, false, singleLevel);
298 // if (singleLevel) {
299 // if (targetSession.hasPendingChanges()) {
301 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
304 // targetSession.save();
306 // // updateMonitor("Checked " + targetNode.getPath(), false);
310 // mixin and properties
311 for (NodeType nt
: sourceNode
.getMixinNodeTypes()) {
312 if (!targetNode
.isNodeType(nt
.getName())
313 && targetNode
.canAddMixin(nt
.getName()))
314 targetNode
.addMixin(nt
.getName());
316 copyProperties(sourceNode
, targetNode
);
319 NodeIterator ni
= sourceNode
.getNodes();
320 while (ni
!= null && ni
.hasNext()) {
321 Node sourceChild
= ni
.nextNode();
322 syncNode(sourceChild
, targetSession
);
325 copyTimestamps(sourceNode
, targetNode
);
327 if (sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
)) {
328 if (targetSession
.hasPendingChanges()) {
329 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
330 updateMonitor((isNew ?
"Added " : "Updated ")
331 + targetNode
.getPath(), true);
333 targetSession
.save();
335 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
336 updateMonitor("Checked " + targetNode
.getPath(), false);
339 } catch (RepositoryException e
) {
340 throw new SlcException("Cannot sync source node " + sourceNode
, e
);
344 private void copyTimestamps(Node sourceNode
, Node targetNode
)
345 throws RepositoryException
{
346 if (sourceNode
.getDefinition().isProtected())
348 if (targetNode
.getDefinition().isProtected())
350 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED
);
351 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED_BY
);
352 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED
);
353 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED_BY
);
356 private void copyTimestamp(Node sourceNode
, Node targetNode
, String property
)
357 throws RepositoryException
{
358 if (sourceNode
.hasProperty(property
)) {
359 Property p
= sourceNode
.getProperty(property
);
360 if (p
.getDefinition().isProtected())
362 if (targetNode
.hasProperty(property
)
364 .getProperty(property
)
366 .equals(sourceNode
.getProperty(property
).getValue()))
368 targetNode
.setProperty(property
, sourceNode
.getProperty(property
)
373 private void copyProperties(Node sourceNode
, Node targetNode
)
374 throws RepositoryException
{
375 properties
: for (PropertyIterator pi
= sourceNode
.getProperties(); pi
377 Property p
= pi
.nextProperty();
378 if (p
.getDefinition().isProtected())
380 if (p
.getName().equals(Property
.JCR_CREATED
)
381 || p
.getName().equals(Property
.JCR_CREATED_BY
)
382 || p
.getName().equals(Property
.JCR_LAST_MODIFIED
)
383 || p
.getName().equals(Property
.JCR_LAST_MODIFIED_BY
))
386 if (p
.getType() == PropertyType
.BINARY
) {
387 copyBinary(p
, targetNode
);
390 if (p
.isMultiple()) {
391 if (!targetNode
.hasProperty(p
.getName())
393 targetNode
.getProperty(p
.getName())
394 .getValues(), p
.getValues()))
395 targetNode
.setProperty(p
.getName(), p
.getValues());
397 if (!targetNode
.hasProperty(p
.getName())
398 || !targetNode
.getProperty(p
.getName()).getValue()
399 .equals(p
.getValue()))
400 targetNode
.setProperty(p
.getName(), p
.getValue());
406 private static void copyBinary(Property p
, Node targetNode
)
407 throws RepositoryException
{
408 InputStream in
= null;
409 Binary sourceBinary
= null;
410 Binary targetBinary
= null;
412 sourceBinary
= p
.getBinary();
413 if (targetNode
.hasProperty(p
.getName()))
414 targetBinary
= targetNode
.getProperty(p
.getName()).getBinary();
416 // optim FIXME make it more configurable
417 if (targetBinary
!= null)
418 if (sourceBinary
.getSize() == targetBinary
.getSize()) {
419 if (log
.isTraceEnabled())
420 log
.trace("Skipped " + p
.getPath());
424 in
= sourceBinary
.getStream();
425 targetBinary
= targetNode
.getSession().getValueFactory()
427 targetNode
.setProperty(p
.getName(), targetBinary
);
428 } catch (Exception e
) {
429 throw new SlcException("Could not transfer " + p
, e
);
431 IOUtils
.closeQuietly(in
);
432 JcrUtils
.closeQuietly(sourceBinary
);
433 JcrUtils
.closeQuietly(targetBinary
);
437 /** factorizes monitor management */
438 private void updateMonitor(String msg
, Boolean doLog
) {
439 if (doLog
&& log
.isDebugEnabled())
441 if (monitor
!= null) {
443 monitor
.subTask(msg
);
447 // private void syncNode_old(Node sourceNode, Node targetParentNode)
448 // throws RepositoryException, SAXException {
450 // // enable cancelation of the current fetch process
451 // // FIXME insure the repository stays in a stable state
452 // if (monitor != null && monitor.isCanceled()) {
453 // updateMonitor("Fetched has been canceled, "
454 // + "process is terminating");
458 // Boolean noRecurse = singleLevel(sourceNode);
459 // Calendar sourceLastModified = null;
460 // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
461 // sourceLastModified = sourceNode.getProperty(
462 // Property.JCR_LAST_MODIFIED).getDate();
465 // if (sourceNode.getDefinition().isProtected())
466 // log.warn(sourceNode + " is protected.");
468 // if (!targetParentNode.hasNode(sourceNode.getName())) {
469 // String msg = "Adding " + sourceNode.getPath();
470 // updateMonitor(msg);
471 // if (log.isDebugEnabled())
473 // ContentHandler contentHandler = targetParentNode
476 // .getImportContentHandler(targetParentNode.getPath(),
477 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
478 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
479 // contentHandler, false, noRecurse);
481 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
482 // if (sourceLastModified != null) {
483 // Calendar targetLastModified = null;
484 // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
485 // targetLastModified = targetNode.getProperty(
486 // Property.JCR_LAST_MODIFIED).getDate();
489 // if (targetLastModified == null
490 // || targetLastModified.before(sourceLastModified)) {
491 // String msg = "Updating " + targetNode.getPath();
492 // updateMonitor(msg);
493 // if (log.isDebugEnabled())
495 // ContentHandler contentHandler = targetParentNode
498 // .getImportContentHandler(
499 // targetParentNode.getPath(),
500 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
501 // sourceNode.getSession().exportSystemView(
502 // sourceNode.getPath(), contentHandler, false,
505 // String msg = "Skipped up to date " + targetNode.getPath();
506 // updateMonitor(msg);
507 // if (log.isDebugEnabled())
516 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
517 // if (sourceLastModified != null) {
518 // Calendar zero = new GregorianCalendar();
519 // zero.setTimeInMillis(0);
520 // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
521 // targetNode.getSession().save();
524 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
525 // syncNode_old(it.nextNode(), targetNode);
528 // if (sourceLastModified != null) {
529 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
530 // sourceLastModified);
531 // targetNode.getSession().save();
536 protected Boolean
singleLevel(Node sourceNode
) throws RepositoryException
{
537 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
543 * Synchronises only one workspace, retrieved by name without changing its
546 public void setSourceWksp(String sourceWksp
) {
547 if (sourceWksp
!= null && !sourceWksp
.trim().equals("")) {
548 Map
<String
, String
> map
= new HashMap
<String
, String
>();
549 map
.put(sourceWksp
, sourceWksp
);
555 * Synchronises a map of workspaces that will be retrieved by name. If the
556 * target name is not defined (eg null or an empty string) for a given
557 * source workspace, we use the source name as target name.
559 public void setWkspMap(Map
<String
, String
> workspaceMap
) {
560 // clean the list to ease later use
561 this.workspaceMap
= new HashMap
<String
, String
>();
562 if (workspaceMap
!= null) {
563 workspaceNames
: for (String srcName
: workspaceMap
.keySet()) {
564 String targetName
= workspaceMap
.get(srcName
);
567 if (srcName
.trim().equals(""))
568 continue workspaceNames
;
569 if (targetName
== null || "".equals(targetName
.trim()))
570 targetName
= srcName
;
571 this.workspaceMap
.put(srcName
, targetName
);
574 // clean the map to ease later use
575 if (this.workspaceMap
.size() == 0)
576 this.workspaceMap
= null;
579 public void setMonitor(ArgeoMonitor monitor
) {
580 this.monitor
= monitor
;
583 public void setRepositoryFactory(RepositoryFactory repositoryFactory
) {
584 this.repositoryFactory
= repositoryFactory
;
587 public void setSourceRepoUri(String sourceRepoUri
) {
588 this.sourceRepoUri
= sourceRepoUri
;
591 public void setSourceUsername(String sourceUsername
) {
592 this.sourceUsername
= sourceUsername
;
595 public void setSourcePassword(char[] sourcePassword
) {
596 this.sourcePassword
= sourcePassword
;
599 public void setTargetRepoUri(String targetRepoUri
) {
600 this.targetRepoUri
= targetRepoUri
;
603 public void setTargetUsername(String targetUsername
) {
604 this.targetUsername
= targetUsername
;
607 public void setTargetPassword(char[] targetPassword
) {
608 this.targetPassword
= targetPassword
;
611 public void setSourceRepository(Repository sourceRepository
) {
612 this.sourceRepository
= sourceRepository
;
615 public void setSourceCredentials(Credentials sourceCredentials
) {
616 this.sourceCredentials
= sourceCredentials
;
619 public void setTargetRepository(Repository targetRepository
) {
620 this.targetRepository
= targetRepository
;
623 public void setTargetCredentials(Credentials targetCredentials
) {
624 this.targetCredentials
= targetCredentials
;
627 public void setFilesOnly(Boolean filesOnly
) {
628 this.filesOnly
= filesOnly
;