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())
217 JcrUtils
.copyFiles(sourceSession
.getRootNode(), targetSession
.getRootNode(), true, monitor
);
219 for (NodeIterator it
= sourceSession
.getRootNode().getNodes(); it
.hasNext();) {
220 Node node
= it
.nextNode();
221 if (node
.getName().equals("jcr:system"))
223 syncNode(node
, targetSession
);
226 if (log
.isDebugEnabled())
227 log
.debug("Synced " + sourceSession
.getWorkspace().getName());
228 } catch (Exception e
) {
230 throw new SlcException("Cannot sync " + sourceSession
.getWorkspace().getName() + " to "
231 + targetSession
.getWorkspace().getName(), e
);
235 /** factorizes monitor management */
236 private void updateMonitor(String msg
) {
237 updateMonitor(msg
, false);
240 protected void syncNode(Node sourceNode
, Session targetSession
) throws RepositoryException
, SAXException
{
241 // Boolean singleLevel = singleLevel(sourceNode);
243 if (monitor
!= null && monitor
.isCanceled()) {
244 updateMonitor("Fetched has been canceled, " + "process is terminating");
248 Node targetParentNode
= targetSession
.getNode(sourceNode
.getParent().getPath());
250 if (monitor
!= null && sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
))
251 monitor
.subTask("Process " + sourceNode
.getPath());
254 if (!targetSession
.itemExists(sourceNode
.getPath())) {
256 targetNode
= targetParentNode
.addNode(sourceNode
.getName(), sourceNode
.getPrimaryNodeType().getName());
259 targetNode
= targetSession
.getNode(sourceNode
.getPath());
260 if (!targetNode
.getPrimaryNodeType().getName().equals(sourceNode
.getPrimaryNodeType().getName()))
261 targetNode
.setPrimaryType(sourceNode
.getPrimaryNodeType().getName());
265 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
266 // contentHandler, false, singleLevel);
268 // if (singleLevel) {
269 // if (targetSession.hasPendingChanges()) {
271 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
274 // targetSession.save();
276 // // updateMonitor("Checked " + targetNode.getPath(), false);
280 // mixin and properties
281 for (NodeType nt
: sourceNode
.getMixinNodeTypes()) {
282 if (!targetNode
.isNodeType(nt
.getName()) && targetNode
.canAddMixin(nt
.getName()))
283 targetNode
.addMixin(nt
.getName());
285 copyProperties(sourceNode
, targetNode
);
288 NodeIterator ni
= sourceNode
.getNodes();
289 while (ni
!= null && ni
.hasNext()) {
290 Node sourceChild
= ni
.nextNode();
291 syncNode(sourceChild
, targetSession
);
294 copyTimestamps(sourceNode
, targetNode
);
296 if (sourceNode
.isNodeType(NodeType
.NT_HIERARCHY_NODE
)) {
297 if (targetSession
.hasPendingChanges()) {
298 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
299 updateMonitor((isNew ?
"Added " : "Updated ") + targetNode
.getPath(), true);
301 targetSession
.save();
303 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
304 updateMonitor("Checked " + targetNode
.getPath(), false);
307 } catch (RepositoryException e
) {
308 throw new SlcException("Cannot sync source node " + sourceNode
, e
);
312 private void copyTimestamps(Node sourceNode
, Node targetNode
) throws RepositoryException
{
313 if (sourceNode
.getDefinition().isProtected())
315 if (targetNode
.getDefinition().isProtected())
317 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED
);
318 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_CREATED_BY
);
319 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED
);
320 copyTimestamp(sourceNode
, targetNode
, Property
.JCR_LAST_MODIFIED_BY
);
323 private void copyTimestamp(Node sourceNode
, Node targetNode
, String property
) throws RepositoryException
{
324 if (sourceNode
.hasProperty(property
)) {
325 Property p
= sourceNode
.getProperty(property
);
326 if (p
.getDefinition().isProtected())
328 if (targetNode
.hasProperty(property
)
329 && targetNode
.getProperty(property
).getValue().equals(sourceNode
.getProperty(property
).getValue()))
331 targetNode
.setProperty(property
, sourceNode
.getProperty(property
).getValue());
335 private void copyProperties(Node sourceNode
, Node targetNode
) throws RepositoryException
{
336 properties
: for (PropertyIterator pi
= sourceNode
.getProperties(); pi
.hasNext();) {
337 Property p
= pi
.nextProperty();
338 if (p
.getDefinition().isProtected())
340 if (p
.getName().equals(Property
.JCR_CREATED
) || p
.getName().equals(Property
.JCR_CREATED_BY
)
341 || p
.getName().equals(Property
.JCR_LAST_MODIFIED
)
342 || p
.getName().equals(Property
.JCR_LAST_MODIFIED_BY
))
345 if (p
.getType() == PropertyType
.BINARY
) {
346 copyBinary(p
, targetNode
);
349 if (p
.isMultiple()) {
350 if (!targetNode
.hasProperty(p
.getName())
351 || !Arrays
.equals(targetNode
.getProperty(p
.getName()).getValues(), p
.getValues()))
352 targetNode
.setProperty(p
.getName(), p
.getValues());
354 if (!targetNode
.hasProperty(p
.getName())
355 || !targetNode
.getProperty(p
.getName()).getValue().equals(p
.getValue()))
356 targetNode
.setProperty(p
.getName(), p
.getValue());
362 private static void copyBinary(Property p
, Node targetNode
) throws RepositoryException
{
363 InputStream in
= null;
364 Binary sourceBinary
= null;
365 Binary targetBinary
= null;
367 sourceBinary
= p
.getBinary();
368 if (targetNode
.hasProperty(p
.getName()))
369 targetBinary
= targetNode
.getProperty(p
.getName()).getBinary();
371 // optim FIXME make it more configurable
372 if (targetBinary
!= null)
373 if (sourceBinary
.getSize() == targetBinary
.getSize()) {
374 if (log
.isTraceEnabled())
375 log
.trace("Skipped " + p
.getPath());
379 in
= sourceBinary
.getStream();
380 targetBinary
= targetNode
.getSession().getValueFactory().createBinary(in
);
381 targetNode
.setProperty(p
.getName(), targetBinary
);
382 } catch (Exception e
) {
383 throw new SlcException("Could not transfer " + p
, e
);
385 IOUtils
.closeQuietly(in
);
386 JcrUtils
.closeQuietly(sourceBinary
);
387 JcrUtils
.closeQuietly(targetBinary
);
391 /** factorizes monitor management */
392 private void updateMonitor(String msg
, Boolean doLog
) {
393 if (doLog
&& log
.isDebugEnabled())
395 if (monitor
!= null) {
397 monitor
.subTask(msg
);
401 // private void syncNode_old(Node sourceNode, Node targetParentNode)
402 // throws RepositoryException, SAXException {
404 // // enable cancelation of the current fetch process
405 // // fxme insure the repository stays in a stable state
406 // if (monitor != null && monitor.isCanceled()) {
407 // updateMonitor("Fetched has been canceled, "
408 // + "process is terminating");
412 // Boolean noRecurse = singleLevel(sourceNode);
413 // Calendar sourceLastModified = null;
414 // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
415 // sourceLastModified = sourceNode.getProperty(
416 // Property.JCR_LAST_MODIFIED).getDate();
419 // if (sourceNode.getDefinition().isProtected())
420 // log.warn(sourceNode + " is protected.");
422 // if (!targetParentNode.hasNode(sourceNode.getName())) {
423 // String msg = "Adding " + sourceNode.getPath();
424 // updateMonitor(msg);
425 // if (log.isDebugEnabled())
427 // ContentHandler contentHandler = targetParentNode
430 // .getImportContentHandler(targetParentNode.getPath(),
431 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
432 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
433 // contentHandler, false, noRecurse);
435 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
436 // if (sourceLastModified != null) {
437 // Calendar targetLastModified = null;
438 // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
439 // targetLastModified = targetNode.getProperty(
440 // Property.JCR_LAST_MODIFIED).getDate();
443 // if (targetLastModified == null
444 // || targetLastModified.before(sourceLastModified)) {
445 // String msg = "Updating " + targetNode.getPath();
446 // updateMonitor(msg);
447 // if (log.isDebugEnabled())
449 // ContentHandler contentHandler = targetParentNode
452 // .getImportContentHandler(
453 // targetParentNode.getPath(),
454 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
455 // sourceNode.getSession().exportSystemView(
456 // sourceNode.getPath(), contentHandler, false,
459 // String msg = "Skipped up to date " + targetNode.getPath();
460 // updateMonitor(msg);
461 // if (log.isDebugEnabled())
470 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
471 // if (sourceLastModified != null) {
472 // Calendar zero = new GregorianCalendar();
473 // zero.setTimeInMillis(0);
474 // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
475 // targetNode.getSession().save();
478 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
479 // syncNode_old(it.nextNode(), targetNode);
482 // if (sourceLastModified != null) {
483 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
484 // sourceLastModified);
485 // targetNode.getSession().save();
490 protected Boolean
singleLevel(Node sourceNode
) throws RepositoryException
{
491 if (sourceNode
.isNodeType(NodeType
.NT_FILE
))
497 * Synchronises only one workspace, retrieved by name without changing its
500 public void setSourceWksp(String sourceWksp
) {
501 if (sourceWksp
!= null && !sourceWksp
.trim().equals("")) {
502 Map
<String
, String
> map
= new HashMap
<String
, String
>();
503 map
.put(sourceWksp
, sourceWksp
);
509 * Synchronises a map of workspaces that will be retrieved by name. If the
510 * target name is not defined (eg null or an empty string) for a given
511 * source workspace, we use the source name as target name.
513 public void setWkspMap(Map
<String
, String
> workspaceMap
) {
514 // clean the list to ease later use
515 this.workspaceMap
= new HashMap
<String
, String
>();
516 if (workspaceMap
!= null) {
517 workspaceNames
: for (String srcName
: workspaceMap
.keySet()) {
518 String targetName
= workspaceMap
.get(srcName
);
521 if (srcName
.trim().equals(""))
522 continue workspaceNames
;
523 if (targetName
== null || "".equals(targetName
.trim()))
524 targetName
= srcName
;
525 this.workspaceMap
.put(srcName
, targetName
);
528 // clean the map to ease later use
529 if (this.workspaceMap
.size() == 0)
530 this.workspaceMap
= null;
533 public void setMonitor(JcrMonitor monitor
) {
534 this.monitor
= monitor
;
537 public void setRepositoryFactory(RepositoryFactory repositoryFactory
) {
538 this.repositoryFactory
= repositoryFactory
;
541 public void setSourceRepoUri(String sourceRepoUri
) {
542 this.sourceRepoUri
= sourceRepoUri
;
545 public void setSourceUsername(String sourceUsername
) {
546 this.sourceUsername
= sourceUsername
;
549 public void setSourcePassword(char[] sourcePassword
) {
550 this.sourcePassword
= sourcePassword
;
553 public void setTargetRepoUri(String targetRepoUri
) {
554 this.targetRepoUri
= targetRepoUri
;
557 public void setTargetUsername(String targetUsername
) {
558 this.targetUsername
= targetUsername
;
561 public void setTargetPassword(char[] targetPassword
) {
562 this.targetPassword
= targetPassword
;
565 public void setSourceRepository(Repository sourceRepository
) {
566 this.sourceRepository
= sourceRepository
;
569 public void setSourceCredentials(Credentials sourceCredentials
) {
570 this.sourceCredentials
= sourceCredentials
;
573 public void setTargetRepository(Repository targetRepository
) {
574 this.targetRepository
= targetRepository
;
577 public void setTargetCredentials(Credentials targetCredentials
) {
578 this.targetCredentials
= targetCredentials
;
581 public void setFilesOnly(Boolean filesOnly
) {
582 this.filesOnly
= filesOnly
;