]> git.argeo.org Git - gpl/argeo-slc.git/blob - org.argeo.slc.repo/src/org/argeo/slc/repo/RepoSync.java
Reference BND explicitly as a Maven dependency.
[gpl/argeo-slc.git] / org.argeo.slc.repo / src / org / argeo / slc / repo / RepoSync.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16 package org.argeo.slc.repo;
17
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;
24 import java.util.Map;
25 import java.util.TimeZone;
26
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;
43
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;
52
53 /**
54 * Synchronise workspaces from a remote software repository to the local
55 * repository (Synchronisation in the other direction does not work).
56 *
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.
60 */
61 public class RepoSync implements Runnable {
62 private final static Log log = LogFactory.getLog(RepoSync.class);
63
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");
66
67 private final Calendar zero;
68 private Session sourceDefaultSession = null;
69 private Session targetDefaultSession = null;
70
71 private Repository sourceRepository;
72 private Credentials sourceCredentials;
73 private Repository targetRepository;
74 private Credentials targetCredentials;
75
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;
83
84 private RepositoryFactory repositoryFactory;
85
86 private JcrMonitor monitor;
87 private Map<String, String> workspaceMap;
88
89 // TODO fix monitor
90 private Boolean filesOnly = false;
91
92 public RepoSync() {
93 zero = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
94 zero.setTimeInMillis(0);
95 }
96
97 /**
98 *
99 * Shortcut to instantiate a RepoSync with already known repositories and
100 * credentials.
101 *
102 * @param sourceRepository
103 * @param sourceCredentials
104 * @param targetRepository
105 * @param targetCredentials
106 */
107 public RepoSync(Repository sourceRepository, Credentials sourceCredentials, Repository targetRepository,
108 Credentials targetCredentials) {
109 this();
110 this.sourceRepository = sourceRepository;
111 this.sourceCredentials = sourceCredentials;
112 this.targetRepository = targetRepository;
113 this.targetCredentials = targetCredentials;
114 }
115
116 public void run() {
117 try {
118 long begin = System.currentTimeMillis();
119
120 // Setup
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);
127
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);
133
134 Map<String, Exception> errors = new HashMap<String, Exception>();
135 for (String sourceWorkspaceName : sourceDefaultSession.getWorkspace().getAccessibleWorkspaceNames()) {
136 if (monitor != null && monitor.isCanceled())
137 break;
138
139 if (workspaceMap != null && !workspaceMap.containsKey(sourceWorkspaceName))
140 continue;
141 if (IGNORED_WKSP_LIST.contains(sourceWorkspaceName))
142 continue;
143
144 Session sourceSession = null;
145 Session targetSession = null;
146 String targetWorkspaceName = workspaceMap.get(sourceWorkspaceName);
147 try {
148 try {
149 targetSession = targetRepository.login(targetCredentials, targetWorkspaceName);
150 } catch (NoSuchWorkspaceException e) {
151 targetDefaultSession.getWorkspace().createWorkspace(targetWorkspaceName);
152 targetSession = targetRepository.login(targetCredentials, targetWorkspaceName);
153 }
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())
159 e.printStackTrace();
160
161 } finally {
162 JcrUtils.logoutQuietly(sourceSession);
163 JcrUtils.logoutQuietly(targetSession);
164 }
165 }
166
167 if (monitor != null && monitor.isCanceled())
168 log.info("Sync has been canceled by user");
169
170 long duration = (System.currentTimeMillis() - begin) / 1000;// s
171 log.info("Sync " + sourceRepoUri + " to " + targetRepoUri + " in " + (duration / 60)
172
173 + "min " + (duration % 60) + "s");
174
175 if (errors.size() > 0) {
176 throw new SlcException("Sync failed " + errors);
177 }
178 } catch (RepositoryException e) {
179 throw new SlcException("Cannot sync " + sourceRepoUri + " to " + targetRepoUri, e);
180 } finally {
181 JcrUtils.logoutQuietly(sourceDefaultSession);
182 JcrUtils.logoutQuietly(targetDefaultSession);
183 }
184 }
185
186 private long getNodesNumber(Session session) {
187 if (IGNORED_WKSP_LIST.contains(session.getWorkspace().getName()))
188 return 0l;
189 try {
190 Query countQuery = session.getWorkspace().getQueryManager().createQuery(
191 "select file from [" + (true ? NodeType.NT_FILE : NodeType.NT_BASE) + "] as file", Query.JCR_SQL2);
192
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);
199 }
200 }
201
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());
207 }
208
209 try {
210 String msg = "Synchronizing workspace: " + sourceSession.getWorkspace().getName();
211 if (monitor != null)
212 monitor.setTaskName(msg);
213 if (log.isDebugEnabled())
214 log.debug(msg);
215
216 for (NodeIterator it = sourceSession.getRootNode().getNodes(); it.hasNext();) {
217 Node node = it.nextNode();
218 if (node.getName().contains(":"))
219 continue;
220 if (node.getName().equals("download"))
221 continue;
222 if (!node.isNodeType(NodeType.NT_HIERARCHY_NODE))
223 continue;
224 syncNode(node, targetSession);
225 }
226 // if (filesOnly) {
227 // JcrUtils.copyFiles(sourceSession.getRootNode(), targetSession.getRootNode(),
228 // true, monitor);
229 // } else {
230 // for (NodeIterator it = sourceSession.getRootNode().getNodes(); it.hasNext();)
231 // {
232 // Node node = it.nextNode();
233 // if (node.getName().equals("jcr:system"))
234 // continue;
235 // syncNode(node, targetSession);
236 // }
237 // }
238 if (log.isDebugEnabled())
239 log.debug("Synced " + sourceSession.getWorkspace().getName());
240 } catch (Exception e) {
241 e.printStackTrace();
242 throw new SlcException("Cannot sync " + sourceSession.getWorkspace().getName() + " to "
243 + targetSession.getWorkspace().getName(), e);
244 }
245 }
246
247 /** factorizes monitor management */
248 private void updateMonitor(String msg) {
249 updateMonitor(msg, false);
250 }
251
252 protected void syncNode(Node sourceNode, Session targetSession) throws RepositoryException, SAXException {
253 if (filesOnly) {
254 Node targetNode;
255 if (targetSession.itemExists(sourceNode.getPath()))
256 targetNode = targetSession.getNode(sourceNode.getPath());
257 else
258 targetNode = JcrUtils.mkdirs(targetSession, sourceNode.getPath(), NodeType.NT_FOLDER);
259 JcrUtils.copyFiles(sourceNode, targetNode, true, monitor, true);
260 return;
261 }
262 // Boolean singleLevel = singleLevel(sourceNode);
263 try {
264 if (monitor != null && monitor.isCanceled()) {
265 updateMonitor("Fetched has been canceled, " + "process is terminating");
266 return;
267 }
268
269 Node targetParentNode = targetSession.getNode(sourceNode.getParent().getPath());
270 Node targetNode;
271 if (monitor != null && sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE))
272 monitor.subTask("Process " + sourceNode.getPath());
273
274 final Boolean isNew;
275 if (!targetSession.itemExists(sourceNode.getPath())) {
276 isNew = true;
277 targetNode = targetParentNode.addNode(sourceNode.getName(), sourceNode.getPrimaryNodeType().getName());
278 } else {
279 isNew = false;
280 targetNode = targetSession.getNode(sourceNode.getPath());
281 if (!targetNode.getPrimaryNodeType().getName().equals(sourceNode.getPrimaryNodeType().getName()))
282 targetNode.setPrimaryType(sourceNode.getPrimaryNodeType().getName());
283 }
284
285 // export
286 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
287 // contentHandler, false, singleLevel);
288
289 // if (singleLevel) {
290 // if (targetSession.hasPendingChanges()) {
291 // // updateMonitor(
292 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
293 // // true);
294 // if (doSave)
295 // targetSession.save();
296 // } else {
297 // // updateMonitor("Checked " + targetNode.getPath(), false);
298 // }
299 // }
300
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());
305 }
306 copyProperties(sourceNode, targetNode);
307
308 // next level
309 NodeIterator ni = sourceNode.getNodes();
310 while (ni != null && ni.hasNext()) {
311 Node sourceChild = ni.nextNode();
312 syncNode(sourceChild, targetSession);
313 }
314
315 copyTimestamps(sourceNode, targetNode);
316
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);
321 // if (doSave)
322 targetSession.save();
323 } else {
324 if (sourceNode.isNodeType(NodeType.NT_FILE))
325 updateMonitor("Checked " + targetNode.getPath(), false);
326 }
327 }
328 } catch (RepositoryException e) {
329 throw new SlcException("Cannot sync source node " + sourceNode, e);
330 }
331 }
332
333 private void copyTimestamps(Node sourceNode, Node targetNode) throws RepositoryException {
334 if (sourceNode.getDefinition().isProtected())
335 return;
336 if (targetNode.getDefinition().isProtected())
337 return;
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);
342 }
343
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())
348 return;
349 if (targetNode.hasProperty(property)
350 && targetNode.getProperty(property).getValue().equals(sourceNode.getProperty(property).getValue()))
351 return;
352 targetNode.setProperty(property, sourceNode.getProperty(property).getValue());
353 }
354 }
355
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())
360 continue properties;
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))
364 continue properties;
365
366 if (p.getType() == PropertyType.BINARY) {
367 copyBinary(p, targetNode);
368 } else {
369
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());
374 } else {
375 if (!targetNode.hasProperty(p.getName())
376 || !targetNode.getProperty(p.getName()).getValue().equals(p.getValue()))
377 targetNode.setProperty(p.getName(), p.getValue());
378 }
379 }
380 }
381 }
382
383 private static void copyBinary(Property p, Node targetNode) throws RepositoryException {
384 InputStream in = null;
385 Binary sourceBinary = null;
386 Binary targetBinary = null;
387 try {
388 sourceBinary = p.getBinary();
389 if (targetNode.hasProperty(p.getName()))
390 targetBinary = targetNode.getProperty(p.getName()).getBinary();
391
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());
397 return;
398 }
399
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);
405 } finally {
406 IOUtils.closeQuietly(in);
407 JcrUtils.closeQuietly(sourceBinary);
408 JcrUtils.closeQuietly(targetBinary);
409 }
410 }
411
412 /** factorizes monitor management */
413 private void updateMonitor(String msg, Boolean doLog) {
414 if (doLog && log.isDebugEnabled())
415 log.debug(msg);
416 if (monitor != null) {
417 monitor.worked(1);
418 monitor.subTask(msg);
419 }
420 }
421
422 // private void syncNode_old(Node sourceNode, Node targetParentNode)
423 // throws RepositoryException, SAXException {
424 //
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");
430 // return;
431 // }
432 //
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();
438 // }
439 //
440 // if (sourceNode.getDefinition().isProtected())
441 // log.warn(sourceNode + " is protected.");
442 //
443 // if (!targetParentNode.hasNode(sourceNode.getName())) {
444 // String msg = "Adding " + sourceNode.getPath();
445 // updateMonitor(msg);
446 // if (log.isDebugEnabled())
447 // log.debug(msg);
448 // ContentHandler contentHandler = targetParentNode
449 // .getSession()
450 // .getWorkspace()
451 // .getImportContentHandler(targetParentNode.getPath(),
452 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
453 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
454 // contentHandler, false, noRecurse);
455 // } else {
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();
462 // }
463 //
464 // if (targetLastModified == null
465 // || targetLastModified.before(sourceLastModified)) {
466 // String msg = "Updating " + targetNode.getPath();
467 // updateMonitor(msg);
468 // if (log.isDebugEnabled())
469 // log.debug(msg);
470 // ContentHandler contentHandler = targetParentNode
471 // .getSession()
472 // .getWorkspace()
473 // .getImportContentHandler(
474 // targetParentNode.getPath(),
475 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
476 // sourceNode.getSession().exportSystemView(
477 // sourceNode.getPath(), contentHandler, false,
478 // noRecurse);
479 // } else {
480 // String msg = "Skipped up to date " + targetNode.getPath();
481 // updateMonitor(msg);
482 // if (log.isDebugEnabled())
483 // log.debug(msg);
484 // return;
485 // }
486 // }
487 // }
488 //
489 // if (noRecurse) {
490 // // recurse
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();
497 // }
498 //
499 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
500 // syncNode_old(it.nextNode(), targetNode);
501 // }
502 //
503 // if (sourceLastModified != null) {
504 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
505 // sourceLastModified);
506 // targetNode.getSession().save();
507 // }
508 // }
509 // }
510
511 protected Boolean singleLevel(Node sourceNode) throws RepositoryException {
512 if (sourceNode.isNodeType(NodeType.NT_FILE))
513 return false;
514 return true;
515 }
516
517 /**
518 * Synchronises only one workspace, retrieved by name without changing its name.
519 */
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);
524 setWkspMap(map);
525 }
526 }
527
528 /**
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.
532 */
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);
539
540 // Sanity check
541 if (srcName.trim().equals(""))
542 continue workspaceNames;
543 if (targetName == null || "".equals(targetName.trim()))
544 targetName = srcName;
545 this.workspaceMap.put(srcName, targetName);
546 }
547 }
548 // clean the map to ease later use
549 if (this.workspaceMap.size() == 0)
550 this.workspaceMap = null;
551 }
552
553 public void setMonitor(JcrMonitor monitor) {
554 this.monitor = monitor;
555 }
556
557 public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
558 this.repositoryFactory = repositoryFactory;
559 }
560
561 public void setSourceRepoUri(String sourceRepoUri) {
562 this.sourceRepoUri = sourceRepoUri;
563 }
564
565 public void setSourceUsername(String sourceUsername) {
566 this.sourceUsername = sourceUsername;
567 }
568
569 public void setSourcePassword(char[] sourcePassword) {
570 this.sourcePassword = sourcePassword;
571 }
572
573 public void setTargetRepoUri(String targetRepoUri) {
574 this.targetRepoUri = targetRepoUri;
575 }
576
577 public void setTargetUsername(String targetUsername) {
578 this.targetUsername = targetUsername;
579 }
580
581 public void setTargetPassword(char[] targetPassword) {
582 this.targetPassword = targetPassword;
583 }
584
585 public void setSourceRepository(Repository sourceRepository) {
586 this.sourceRepository = sourceRepository;
587 }
588
589 public void setSourceCredentials(Credentials sourceCredentials) {
590 this.sourceCredentials = sourceCredentials;
591 }
592
593 public void setTargetRepository(Repository targetRepository) {
594 this.targetRepository = targetRepository;
595 }
596
597 public void setTargetCredentials(Credentials targetCredentials) {
598 this.targetCredentials = targetCredentials;
599 }
600
601 public void setFilesOnly(Boolean filesOnly) {
602 this.filesOnly = filesOnly;
603 }
604
605 }