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
.ArrayList
;
20 import java
.util
.Arrays
;
21 import java
.util
.Calendar
;
22 import java
.util
.GregorianCalendar
;
23 import java
.util
.HashMap
;
24 import java
.util
.List
;
26 import java
.util
.TimeZone
;
28 import javax
.jcr
.Binary
;
29 import javax
.jcr
.Credentials
;
30 import javax
.jcr
.NoSuchWorkspaceException
;
31 import javax
.jcr
.Node
;
32 import javax
.jcr
.NodeIterator
;
33 import javax
.jcr
.Property
;
34 import javax
.jcr
.PropertyIterator
;
35 import javax
.jcr
.PropertyType
;
36 import javax
.jcr
.Repository
;
37 import javax
.jcr
.RepositoryException
;
38 import javax
.jcr
.RepositoryFactory
;
39 import javax
.jcr
.Session
;
40 import javax
.jcr
.SimpleCredentials
;
41 import javax
.jcr
.nodetype
.NodeType
;
42 import javax
.jcr
.query
.Query
;
43 import javax
.jcr
.query
.QueryResult
;
45 import org
.apache
.commons
.io
.IOUtils
;
46 import org
.apache
.commons
.logging
.Log
;
47 import org
.apache
.commons
.logging
.LogFactory
;
48 import org
.argeo
.ArgeoMonitor
;
49 import org
.argeo
.jcr
.ArgeoJcrUtils
;
50 import org
.argeo
.jcr
.JcrUtils
;
51 import org
.argeo
.slc
.SlcException
;
52 import org
.xml
.sax
.SAXException
;
54 /** Sync to from software repositories */
55 public class RepoSync
implements Runnable
{
56 private final static Log log
= LogFactory
.getLog(RepoSync
.class);
58 // Centralizes definition of workspaces that must be ignored by the sync.
59 private final static List
<String
> IGNORED_WSKP_LIST
= Arrays
.asList(
60 "security", "localrepo");
62 private final Calendar zero
;
63 private Session sourceDefaultSession
= null;
64 private Session targetDefaultSession
= null;
66 private Repository sourceRepository
;
67 private Credentials sourceCredentials
;
68 private Repository targetRepository
;
69 private Credentials targetCredentials
;
71 // if Repository and Credentials objects are not explicitly set
72 private String sourceRepoUri
;
73 private String sourceUsername
;
74 private char[] sourcePassword
;
75 private String targetRepoUri
;
76 private String targetUsername
;
77 private char[] targetPassword
;
79 private RepositoryFactory repositoryFactory
;
81 private ArgeoMonitor monitor
;
82 private List
<String
> sourceWkspList
;
85 private Boolean filesOnly
= false;
88 zero
= new GregorianCalendar(TimeZone
.getTimeZone("UTC"));
89 zero
.setTimeInMillis(0);
94 * Shortcut to instantiate a RepoSync with already known repositories and
97 * @param sourceRepository
98 * @param sourceCredentials
99 * @param targetRepository
100 * @param targetCredentials
102 public RepoSync(Repository sourceRepository
, Credentials sourceCredentials
,
103 Repository targetRepository
, Credentials targetCredentials
) {
105 this.sourceRepository
= sourceRepository
;
106 this.sourceCredentials
= sourceCredentials
;
107 this.targetRepository
= targetRepository
;
108 this.targetCredentials
= targetCredentials
;
113 long begin
= System
.currentTimeMillis();
116 if (sourceRepository
== null)
117 sourceRepository
= ArgeoJcrUtils
.getRepositoryByUri(
118 repositoryFactory
, sourceRepoUri
);
119 if (sourceCredentials
== null && sourceUsername
!= null)
120 sourceCredentials
= new SimpleCredentials(sourceUsername
,
122 sourceDefaultSession
= sourceRepository
.login(sourceCredentials
);
124 if (targetRepository
== null)
125 targetRepository
= ArgeoJcrUtils
.getRepositoryByUri(
126 repositoryFactory
, targetRepoUri
);
127 if (targetCredentials
== null && targetUsername
!= null)
128 targetCredentials
= new SimpleCredentials(targetUsername
,
130 targetDefaultSession
= targetRepository
.login(targetCredentials
);
132 Map
<String
, Exception
> errors
= new HashMap
<String
, Exception
>();
133 for (String sourceWorkspaceName
: sourceDefaultSession
134 .getWorkspace().getAccessibleWorkspaceNames()) {
135 if (monitor
!= null && monitor
.isCanceled())
138 if (sourceWkspList
!= null
139 && !sourceWkspList
.contains(sourceWorkspaceName
))
141 if (IGNORED_WSKP_LIST
.contains(sourceWorkspaceName
))
144 Session sourceSession
= null;
145 Session targetSession
= null;
148 targetSession
= targetRepository
.login(
149 targetCredentials
, sourceWorkspaceName
);
150 } catch (NoSuchWorkspaceException e
) {
151 targetDefaultSession
.getWorkspace().createWorkspace(
152 sourceWorkspaceName
);
153 targetSession
= targetRepository
.login(
154 targetCredentials
, sourceWorkspaceName
);
156 sourceSession
= sourceRepository
.login(sourceCredentials
,
157 sourceWorkspaceName
);
158 syncWorkspace(sourceSession
, targetSession
);
159 } catch (Exception e
) {
160 errors
.put("Could not sync workspace "
161 + sourceWorkspaceName
, e
);
162 if (log
.isDebugEnabled())
165 JcrUtils
.logoutQuietly(sourceSession
);
166 JcrUtils
.logoutQuietly(targetSession
);
170 if (monitor
!= null && monitor
.isCanceled())
171 log
.info("Sync has been canceled by user");
173 long duration
= (System
.currentTimeMillis() - begin
) / 1000;// s
174 log
.info("Sync " + sourceRepoUri
+ " to " + targetRepoUri
+ " in "
177 + "min " + (duration
% 60) + "s");
179 if (errors
.size() > 0) {
180 throw new SlcException("Sync failed " + errors
);
182 } catch (RepositoryException e
) {
183 throw new SlcException("Cannot sync " + sourceRepoUri
+ " to "
186 JcrUtils
.logoutQuietly(sourceDefaultSession
);
187 JcrUtils
.logoutQuietly(targetDefaultSession
);
191 private long getNodesNumber(Session session
) {
192 if (IGNORED_WSKP_LIST
.contains(session
.getWorkspace().getName()))
194 Session sourceSession
= null;
196 Query countQuery
= session
201 + (true ?
"nt:file" : "nt:base")
202 + "] as file", Query
.JCR_SQL2
);
203 QueryResult result
= countQuery
.execute();
204 Long expectedCount
= result
.getNodes().getSize();
205 return expectedCount
;
206 } catch (RepositoryException e
) {
207 throw new SlcException("Unexpected error while computing "
208 + "the size of the fetch for workspace "
209 + session
.getWorkspace().getName(), e
);
211 JcrUtils
.logoutQuietly(sourceSession
);
215 protected void syncWorkspace(Session sourceSession
, Session targetSession
) {
216 if (monitor
!= null) {
217 monitor
.beginTask("Computing fetch size...", -1);
218 Long totalAmount
= getNodesNumber(sourceSession
);
219 // if (sourceWkspList != null) {
220 // for (String wkspName : sourceWkspList) {
221 // totalAmount += getNodesNumber(wkspName);
224 // for (String sourceWorkspaceName : sourceDefaultSession
225 // .getWorkspace().getAccessibleWorkspaceNames()) {
226 // totalAmount += getNodesNumber(sourceWorkspaceName);
228 monitor
.beginTask("Fetch", totalAmount
.intValue());
230 // if (log.isDebugEnabled())
231 // log.debug("Nb of nodes to sync: " + totalAmount.intValue());
235 String msg
= "Synchronizing workspace: "
236 + sourceSession
.getWorkspace().getName();
238 monitor
.setTaskName(msg
);
239 if (log
.isDebugEnabled())
243 JcrUtils
.copyFiles(sourceSession
.getRootNode(),
244 targetSession
.getRootNode(), true, monitor
);
246 for (NodeIterator it
= sourceSession
.getRootNode().getNodes(); it
248 Node node
= it
.nextNode();
249 if (node
.getName().equals("jcr:system"))
251 syncNode(node
, targetSession
);
254 if (log
.isDebugEnabled())
255 log
.debug("Synced " + sourceSession
.getWorkspace().getName());
256 } catch (Exception e
) {
257 throw new SlcException("Cannot sync "
258 + sourceSession
.getWorkspace().getName() + " to "
259 + targetSession
.getWorkspace().getName(), e
);
263 /** factorizes monitor management */
264 private void updateMonitor(String msg
) {
265 updateMonitor(msg
, false);
268 protected void syncNode(Node sourceNode
, Session targetSession
)
269 throws RepositoryException
, SAXException
{
270 // Boolean singleLevel = singleLevel(sourceNode);
272 if (monitor
!= null && monitor
.isCanceled()) {
273 updateMonitor("Fetched has been canceled, "
274 + "process is terminating");
278 Node targetParentNode
= targetSession
.getNode(sourceNode
.getParent()
282 && sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
))
283 monitor
.subTask("Process " + sourceNode
.getPath());
286 if (!targetSession
.itemExists(sourceNode
.getPath())) {
288 targetNode
= targetParentNode
.addNode(sourceNode
.getName(),
289 sourceNode
.getPrimaryNodeType().getName());
292 targetNode
= targetSession
.getNode(sourceNode
.getPath());
293 if (!targetNode
.getPrimaryNodeType().getName()
294 .equals(sourceNode
.getPrimaryNodeType().getName()))
295 targetNode
.setPrimaryType(sourceNode
.getPrimaryNodeType()
300 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
301 // contentHandler, false, singleLevel);
303 // if (singleLevel) {
304 // if (targetSession.hasPendingChanges()) {
306 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
309 // targetSession.save();
311 // // updateMonitor("Checked " + targetNode.getPath(), false);
315 // mixin and properties
316 for (NodeType nt
: sourceNode
.getMixinNodeTypes()) {
317 if (!targetNode
.isNodeType(nt
.getName())
318 && targetNode
.canAddMixin(nt
.getName()))
319 targetNode
.addMixin(nt
.getName());
321 copyProperties(sourceNode
, targetNode
);
324 for (NodeIterator ni
= sourceNode
.getNodes(); ni
.hasNext();) {
325 Node sourceChild
= ni
.nextNode();
326 syncNode(sourceChild
, targetSession
);
329 copyTimestamps(sourceNode
, targetNode
);
331 if (sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
)) {
332 if (targetSession
.hasPendingChanges()) {
333 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
335 (isNew ?
"Added " : "Updated ")
336 + targetNode
.getPath(), true);
338 targetSession
.save();
340 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
341 updateMonitor("Checked " + targetNode
.getPath(), false);
346 private void copyTimestamps(Node sourceNode
, Node targetNode
)
347 throws RepositoryException
{
348 if (sourceNode
.getDefinition().isProtected())
350 if (targetNode
.getDefinition().isProtected())
352 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED
);
353 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED_BY
);
354 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED
);
355 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED_BY
);
358 private void copyTimestamp(Node sourceNode
, Node targetNode
, String property
)
359 throws RepositoryException
{
360 if (sourceNode
.hasProperty(property
)) {
361 Property p
= sourceNode
.getProperty(property
);
362 if (p
.getDefinition().isProtected())
364 if (targetNode
.hasProperty(property
)
366 .getProperty(property
)
368 .equals(sourceNode
.getProperty(property
).getValue()))
370 targetNode
.setProperty(property
, sourceNode
.getProperty(property
)
375 private void copyProperties(Node sourceNode
, Node targetNode
)
376 throws RepositoryException
{
377 properties
: for (PropertyIterator pi
= sourceNode
.getProperties(); pi
379 Property p
= pi
.nextProperty();
380 if (p
.getDefinition().isProtected())
382 if (p
.getName().equals(Property
.JCR_CREATED
)
383 || p
.getName().equals(Property
.JCR_CREATED_BY
)
384 || p
.getName().equals(Property
.JCR_LAST_MODIFIED
)
385 || p
.getName().equals(Property
.JCR_LAST_MODIFIED_BY
))
388 if (p
.getType() == PropertyType
.BINARY
) {
389 copyBinary(p
, targetNode
);
392 if (p
.isMultiple()) {
393 if (!targetNode
.hasProperty(p
.getName())
395 targetNode
.getProperty(p
.getName())
396 .getValues(), p
.getValues()))
397 targetNode
.setProperty(p
.getName(), p
.getValues());
399 if (!targetNode
.hasProperty(p
.getName())
400 || !targetNode
.getProperty(p
.getName()).getValue()
401 .equals(p
.getValue()))
402 targetNode
.setProperty(p
.getName(), p
.getValue());
408 private static void copyBinary(Property p
, Node targetNode
)
409 throws RepositoryException
{
410 InputStream in
= null;
411 Binary sourceBinary
= null;
412 Binary targetBinary
= null;
414 sourceBinary
= p
.getBinary();
415 if (targetNode
.hasProperty(p
.getName()))
416 targetBinary
= targetNode
.getProperty(p
.getName()).getBinary();
418 // optim FIXME make it more configurable
419 if (targetBinary
!= null)
420 if (sourceBinary
.getSize() == targetBinary
.getSize()) {
421 if (log
.isTraceEnabled())
422 log
.trace("Skipped " + p
.getPath());
426 in
= sourceBinary
.getStream();
427 targetBinary
= targetNode
.getSession().getValueFactory()
429 targetNode
.setProperty(p
.getName(), targetBinary
);
430 } catch (Exception e
) {
431 throw new SlcException("Could not transfer " + p
, e
);
433 IOUtils
.closeQuietly(in
);
434 JcrUtils
.closeQuietly(sourceBinary
);
435 JcrUtils
.closeQuietly(targetBinary
);
439 /** factorizes monitor management */
440 private void updateMonitor(String msg
, Boolean doLog
) {
441 if (doLog
&& log
.isDebugEnabled())
443 if (monitor
!= null) {
445 monitor
.subTask(msg
);
449 // private void syncNode_old(Node sourceNode, Node targetParentNode)
450 // throws RepositoryException, SAXException {
452 // // enable cancelation of the current fetch process
453 // // FIXME insure the repository stays in a stable state
454 // if (monitor != null && monitor.isCanceled()) {
455 // updateMonitor("Fetched has been canceled, "
456 // + "process is terminating");
460 // Boolean noRecurse = singleLevel(sourceNode);
461 // Calendar sourceLastModified = null;
462 // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
463 // sourceLastModified = sourceNode.getProperty(
464 // Property.JCR_LAST_MODIFIED).getDate();
467 // if (sourceNode.getDefinition().isProtected())
468 // log.warn(sourceNode + " is protected.");
470 // if (!targetParentNode.hasNode(sourceNode.getName())) {
471 // String msg = "Adding " + sourceNode.getPath();
472 // updateMonitor(msg);
473 // if (log.isDebugEnabled())
475 // ContentHandler contentHandler = targetParentNode
478 // .getImportContentHandler(targetParentNode.getPath(),
479 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
480 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
481 // contentHandler, false, noRecurse);
483 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
484 // if (sourceLastModified != null) {
485 // Calendar targetLastModified = null;
486 // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
487 // targetLastModified = targetNode.getProperty(
488 // Property.JCR_LAST_MODIFIED).getDate();
491 // if (targetLastModified == null
492 // || targetLastModified.before(sourceLastModified)) {
493 // String msg = "Updating " + targetNode.getPath();
494 // updateMonitor(msg);
495 // if (log.isDebugEnabled())
497 // ContentHandler contentHandler = targetParentNode
500 // .getImportContentHandler(
501 // targetParentNode.getPath(),
502 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
503 // sourceNode.getSession().exportSystemView(
504 // sourceNode.getPath(), contentHandler, false,
507 // String msg = "Skipped up to date " + targetNode.getPath();
508 // updateMonitor(msg);
509 // if (log.isDebugEnabled())
518 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
519 // if (sourceLastModified != null) {
520 // Calendar zero = new GregorianCalendar();
521 // zero.setTimeInMillis(0);
522 // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
523 // targetNode.getSession().save();
526 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
527 // syncNode_old(it.nextNode(), targetNode);
530 // if (sourceLastModified != null) {
531 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
532 // sourceLastModified);
533 // targetNode.getSession().save();
538 protected Boolean
singleLevel(Node sourceNode
) throws RepositoryException
{
539 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
544 /** synchronise only one workspace retrieved by name */
545 public void setSourceWksp(String sourceWksp
) {
546 if (sourceWksp
!= null && !sourceWksp
.trim().equals("")) {
547 List
<String
> list
= new ArrayList
<String
>();
548 list
.add(sourceWksp
);
549 setSourceWkspList(list
);
553 /** synchronise a list workspace that will be retrieved by name */
554 public void setSourceWkspList(List
<String
> sourceWkspList
) {
555 // clean the list to ease later use
556 this.sourceWkspList
= null;
557 if (sourceWkspList
!= null) {
558 for (String wkspName
: sourceWkspList
) {
559 if (!wkspName
.trim().equals("")) {
560 // only instantiate if needed
561 if (this.sourceWkspList
== null)
562 this.sourceWkspList
= new ArrayList
<String
>();
563 this.sourceWkspList
.add(wkspName
);
569 public void setMonitor(ArgeoMonitor monitor
) {
570 this.monitor
= monitor
;
573 public void setRepositoryFactory(RepositoryFactory repositoryFactory
) {
574 this.repositoryFactory
= repositoryFactory
;
577 public void setSourceRepoUri(String sourceRepoUri
) {
578 this.sourceRepoUri
= sourceRepoUri
;
581 public void setSourceUsername(String sourceUsername
) {
582 this.sourceUsername
= sourceUsername
;
585 public void setSourcePassword(char[] sourcePassword
) {
586 this.sourcePassword
= sourcePassword
;
589 public void setTargetRepoUri(String targetRepoUri
) {
590 this.targetRepoUri
= targetRepoUri
;
593 public void setTargetUsername(String targetUsername
) {
594 this.targetUsername
= targetUsername
;
597 public void setTargetPassword(char[] targetPassword
) {
598 this.targetPassword
= targetPassword
;
601 public void setSourceRepository(Repository sourceRepository
) {
602 this.sourceRepository
= sourceRepository
;
605 public void setSourceCredentials(Credentials sourceCredentials
) {
606 this.sourceCredentials
= sourceCredentials
;
609 public void setTargetRepository(Repository targetRepository
) {
610 this.targetRepository
= targetRepository
;
613 public void setTargetCredentials(Credentials targetCredentials
) {
614 this.targetCredentials
= targetCredentials
;
617 public void setFilesOnly(Boolean filesOnly
) {
618 this.filesOnly
= filesOnly
;