]> git.argeo.org Git - gpl/argeo-slc.git/blob - runtime/org.argeo.slc.repo/src/main/java/org/argeo/slc/repo/RepoSync.java
8d0147ec00c3259be9b0d608531fefc38ce3a30a
[gpl/argeo-slc.git] / runtime / org.argeo.slc.repo / src / main / java / 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.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;
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(
66 "security", "localrepo");
67
68 private final Calendar zero;
69 private Session sourceDefaultSession = null;
70 private Session targetDefaultSession = null;
71
72 private Repository sourceRepository;
73 private Credentials sourceCredentials;
74 private Repository targetRepository;
75 private Credentials targetCredentials;
76
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;
84
85 private RepositoryFactory repositoryFactory;
86
87 private ArgeoMonitor monitor;
88 private Map<String, String> workspaceMap;
89
90 // TODO fix monitor
91 private Boolean filesOnly = false;
92
93 public RepoSync() {
94 zero = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
95 zero.setTimeInMillis(0);
96 }
97
98 /**
99 *
100 * Shortcut to instantiate a RepoSync with already known repositories and
101 * credentials.
102 *
103 * @param sourceRepository
104 * @param sourceCredentials
105 * @param targetRepository
106 * @param targetCredentials
107 */
108 public RepoSync(Repository sourceRepository, Credentials sourceCredentials,
109 Repository targetRepository, Credentials targetCredentials) {
110 this();
111 this.sourceRepository = sourceRepository;
112 this.sourceCredentials = sourceCredentials;
113 this.targetRepository = targetRepository;
114 this.targetCredentials = targetCredentials;
115 }
116
117 public void run() {
118 try {
119 long begin = System.currentTimeMillis();
120
121 // Setup
122 if (sourceRepository == null)
123 sourceRepository = ArgeoJcrUtils.getRepositoryByUri(
124 repositoryFactory, sourceRepoUri);
125 if (sourceCredentials == null && sourceUsername != null)
126 sourceCredentials = new SimpleCredentials(sourceUsername,
127 sourcePassword);
128 sourceDefaultSession = sourceRepository.login(sourceCredentials);
129
130 if (targetRepository == null)
131 targetRepository = ArgeoJcrUtils.getRepositoryByUri(
132 repositoryFactory, targetRepoUri);
133 if (targetCredentials == null && targetUsername != null)
134 targetCredentials = new SimpleCredentials(targetUsername,
135 targetPassword);
136 targetDefaultSession = targetRepository.login(targetCredentials);
137
138 Map<String, Exception> errors = new HashMap<String, Exception>();
139 for (String sourceWorkspaceName : sourceDefaultSession
140 .getWorkspace().getAccessibleWorkspaceNames()) {
141 if (monitor != null && monitor.isCanceled())
142 break;
143
144 if (workspaceMap != null
145 && !workspaceMap.containsKey(sourceWorkspaceName))
146 continue;
147 if (IGNORED_WKSP_LIST.contains(sourceWorkspaceName))
148 continue;
149
150 Session sourceSession = null;
151 Session targetSession = null;
152 String targetWorkspaceName = workspaceMap
153 .get(sourceWorkspaceName);
154 try {
155 try {
156 targetSession = targetRepository.login(
157 targetCredentials, targetWorkspaceName);
158 } catch (NoSuchWorkspaceException e) {
159 targetDefaultSession.getWorkspace().createWorkspace(
160 targetWorkspaceName);
161 targetSession = targetRepository.login(
162 targetCredentials, targetWorkspaceName);
163 }
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())
171 e.printStackTrace();
172
173 } finally {
174 JcrUtils.logoutQuietly(sourceSession);
175 JcrUtils.logoutQuietly(targetSession);
176 }
177 }
178
179 if (monitor != null && monitor.isCanceled())
180 log.info("Sync has been canceled by user");
181
182 long duration = (System.currentTimeMillis() - begin) / 1000;// s
183 log.info("Sync " + sourceRepoUri + " to " + targetRepoUri + " in "
184 + (duration / 60)
185
186 + "min " + (duration % 60) + "s");
187
188 if (errors.size() > 0) {
189 throw new SlcException("Sync failed " + errors);
190 }
191 } catch (RepositoryException e) {
192 throw new SlcException("Cannot sync " + sourceRepoUri + " to "
193 + targetRepoUri, e);
194 } finally {
195 JcrUtils.logoutQuietly(sourceDefaultSession);
196 JcrUtils.logoutQuietly(targetDefaultSession);
197 }
198 }
199
200 private long getNodesNumber(Session session) {
201 if (IGNORED_WKSP_LIST.contains(session.getWorkspace().getName()))
202 return 0l;
203 try {
204 Query countQuery = session
205 .getWorkspace()
206 .getQueryManager()
207 .createQuery(
208 "select file from ["
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);
218 }
219 }
220
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());
226 }
227
228 try {
229 String msg = "Synchronizing workspace: "
230 + sourceSession.getWorkspace().getName();
231 if (monitor != null)
232 monitor.setTaskName(msg);
233 if (log.isDebugEnabled())
234 log.debug(msg);
235
236 if (filesOnly) {
237 JcrUtils.copyFiles(sourceSession.getRootNode(),
238 targetSession.getRootNode(), true, monitor);
239 } else {
240 for (NodeIterator it = sourceSession.getRootNode().getNodes(); it
241 .hasNext();) {
242 Node node = it.nextNode();
243 if (node.getName().equals("jcr:system"))
244 continue;
245 syncNode(node, targetSession);
246 }
247 }
248 if (log.isDebugEnabled())
249 log.debug("Synced " + sourceSession.getWorkspace().getName());
250 } catch (Exception e) {
251 throw new SlcException("Cannot sync "
252 + sourceSession.getWorkspace().getName() + " to "
253 + targetSession.getWorkspace().getName(), e);
254 }
255 }
256
257 /** factorizes monitor management */
258 private void updateMonitor(String msg) {
259 updateMonitor(msg, false);
260 }
261
262 protected void syncNode(Node sourceNode, Session targetSession)
263 throws RepositoryException, SAXException {
264 // Boolean singleLevel = singleLevel(sourceNode);
265
266 if (monitor != null && monitor.isCanceled()) {
267 updateMonitor("Fetched has been canceled, "
268 + "process is terminating");
269 return;
270 }
271
272 Node targetParentNode = targetSession.getNode(sourceNode.getParent()
273 .getPath());
274 Node targetNode;
275 if (monitor != null
276 && sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE))
277 monitor.subTask("Process " + sourceNode.getPath());
278
279 final Boolean isNew;
280 if (!targetSession.itemExists(sourceNode.getPath())) {
281 isNew = true;
282 targetNode = targetParentNode.addNode(sourceNode.getName(),
283 sourceNode.getPrimaryNodeType().getName());
284 } else {
285 isNew = false;
286 targetNode = targetSession.getNode(sourceNode.getPath());
287 if (!targetNode.getPrimaryNodeType().getName()
288 .equals(sourceNode.getPrimaryNodeType().getName()))
289 targetNode.setPrimaryType(sourceNode.getPrimaryNodeType()
290 .getName());
291 }
292
293 // export
294 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
295 // contentHandler, false, singleLevel);
296
297 // if (singleLevel) {
298 // if (targetSession.hasPendingChanges()) {
299 // // updateMonitor(
300 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
301 // // true);
302 // if (doSave)
303 // targetSession.save();
304 // } else {
305 // // updateMonitor("Checked " + targetNode.getPath(), false);
306 // }
307 // }
308
309 // mixin and properties
310 for (NodeType nt : sourceNode.getMixinNodeTypes()) {
311 if (!targetNode.isNodeType(nt.getName())
312 && targetNode.canAddMixin(nt.getName()))
313 targetNode.addMixin(nt.getName());
314 }
315 copyProperties(sourceNode, targetNode);
316
317 // next level
318 for (NodeIterator ni = sourceNode.getNodes(); ni.hasNext();) {
319 Node sourceChild = ni.nextNode();
320 syncNode(sourceChild, targetSession);
321 }
322
323 copyTimestamps(sourceNode, targetNode);
324
325 if (sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE)) {
326 if (targetSession.hasPendingChanges()) {
327 if (sourceNode.isNodeType(NodeType.NT_FILE))
328 updateMonitor(
329 (isNew ? "Added " : "Updated ")
330 + targetNode.getPath(), true);
331 // if (doSave)
332 targetSession.save();
333 } else {
334 if (sourceNode.isNodeType(NodeType.NT_FILE))
335 updateMonitor("Checked " + targetNode.getPath(), false);
336 }
337 }
338 }
339
340 private void copyTimestamps(Node sourceNode, Node targetNode)
341 throws RepositoryException {
342 if (sourceNode.getDefinition().isProtected())
343 return;
344 if (targetNode.getDefinition().isProtected())
345 return;
346 copyTimestamp(sourceNode, targetNode, Property.JCR_CREATED);
347 copyTimestamp(sourceNode, targetNode, Property.JCR_CREATED_BY);
348 copyTimestamp(sourceNode, targetNode, Property.JCR_LAST_MODIFIED);
349 copyTimestamp(sourceNode, targetNode, Property.JCR_LAST_MODIFIED_BY);
350 }
351
352 private void copyTimestamp(Node sourceNode, Node targetNode, String property)
353 throws RepositoryException {
354 if (sourceNode.hasProperty(property)) {
355 Property p = sourceNode.getProperty(property);
356 if (p.getDefinition().isProtected())
357 return;
358 if (targetNode.hasProperty(property)
359 && targetNode
360 .getProperty(property)
361 .getValue()
362 .equals(sourceNode.getProperty(property).getValue()))
363 return;
364 targetNode.setProperty(property, sourceNode.getProperty(property)
365 .getValue());
366 }
367 }
368
369 private void copyProperties(Node sourceNode, Node targetNode)
370 throws RepositoryException {
371 properties: for (PropertyIterator pi = sourceNode.getProperties(); pi
372 .hasNext();) {
373 Property p = pi.nextProperty();
374 if (p.getDefinition().isProtected())
375 continue properties;
376 if (p.getName().equals(Property.JCR_CREATED)
377 || p.getName().equals(Property.JCR_CREATED_BY)
378 || p.getName().equals(Property.JCR_LAST_MODIFIED)
379 || p.getName().equals(Property.JCR_LAST_MODIFIED_BY))
380 continue properties;
381
382 if (p.getType() == PropertyType.BINARY) {
383 copyBinary(p, targetNode);
384 } else {
385
386 if (p.isMultiple()) {
387 if (!targetNode.hasProperty(p.getName())
388 || !Arrays.equals(
389 targetNode.getProperty(p.getName())
390 .getValues(), p.getValues()))
391 targetNode.setProperty(p.getName(), p.getValues());
392 } else {
393 if (!targetNode.hasProperty(p.getName())
394 || !targetNode.getProperty(p.getName()).getValue()
395 .equals(p.getValue()))
396 targetNode.setProperty(p.getName(), p.getValue());
397 }
398 }
399 }
400 }
401
402 private static void copyBinary(Property p, Node targetNode)
403 throws RepositoryException {
404 InputStream in = null;
405 Binary sourceBinary = null;
406 Binary targetBinary = null;
407 try {
408 sourceBinary = p.getBinary();
409 if (targetNode.hasProperty(p.getName()))
410 targetBinary = targetNode.getProperty(p.getName()).getBinary();
411
412 // optim FIXME make it more configurable
413 if (targetBinary != null)
414 if (sourceBinary.getSize() == targetBinary.getSize()) {
415 if (log.isTraceEnabled())
416 log.trace("Skipped " + p.getPath());
417 return;
418 }
419
420 in = sourceBinary.getStream();
421 targetBinary = targetNode.getSession().getValueFactory()
422 .createBinary(in);
423 targetNode.setProperty(p.getName(), targetBinary);
424 } catch (Exception e) {
425 throw new SlcException("Could not transfer " + p, e);
426 } finally {
427 IOUtils.closeQuietly(in);
428 JcrUtils.closeQuietly(sourceBinary);
429 JcrUtils.closeQuietly(targetBinary);
430 }
431 }
432
433 /** factorizes monitor management */
434 private void updateMonitor(String msg, Boolean doLog) {
435 if (doLog && log.isDebugEnabled())
436 log.debug(msg);
437 if (monitor != null) {
438 monitor.worked(1);
439 monitor.subTask(msg);
440 }
441 }
442
443 // private void syncNode_old(Node sourceNode, Node targetParentNode)
444 // throws RepositoryException, SAXException {
445 //
446 // // enable cancelation of the current fetch process
447 // // FIXME insure the repository stays in a stable state
448 // if (monitor != null && monitor.isCanceled()) {
449 // updateMonitor("Fetched has been canceled, "
450 // + "process is terminating");
451 // return;
452 // }
453 //
454 // Boolean noRecurse = singleLevel(sourceNode);
455 // Calendar sourceLastModified = null;
456 // if (sourceNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
457 // sourceLastModified = sourceNode.getProperty(
458 // Property.JCR_LAST_MODIFIED).getDate();
459 // }
460 //
461 // if (sourceNode.getDefinition().isProtected())
462 // log.warn(sourceNode + " is protected.");
463 //
464 // if (!targetParentNode.hasNode(sourceNode.getName())) {
465 // String msg = "Adding " + sourceNode.getPath();
466 // updateMonitor(msg);
467 // if (log.isDebugEnabled())
468 // log.debug(msg);
469 // ContentHandler contentHandler = targetParentNode
470 // .getSession()
471 // .getWorkspace()
472 // .getImportContentHandler(targetParentNode.getPath(),
473 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
474 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
475 // contentHandler, false, noRecurse);
476 // } else {
477 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
478 // if (sourceLastModified != null) {
479 // Calendar targetLastModified = null;
480 // if (targetNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
481 // targetLastModified = targetNode.getProperty(
482 // Property.JCR_LAST_MODIFIED).getDate();
483 // }
484 //
485 // if (targetLastModified == null
486 // || targetLastModified.before(sourceLastModified)) {
487 // String msg = "Updating " + targetNode.getPath();
488 // updateMonitor(msg);
489 // if (log.isDebugEnabled())
490 // log.debug(msg);
491 // ContentHandler contentHandler = targetParentNode
492 // .getSession()
493 // .getWorkspace()
494 // .getImportContentHandler(
495 // targetParentNode.getPath(),
496 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
497 // sourceNode.getSession().exportSystemView(
498 // sourceNode.getPath(), contentHandler, false,
499 // noRecurse);
500 // } else {
501 // String msg = "Skipped up to date " + targetNode.getPath();
502 // updateMonitor(msg);
503 // if (log.isDebugEnabled())
504 // log.debug(msg);
505 // return;
506 // }
507 // }
508 // }
509 //
510 // if (noRecurse) {
511 // // recurse
512 // Node targetNode = targetParentNode.getNode(sourceNode.getName());
513 // if (sourceLastModified != null) {
514 // Calendar zero = new GregorianCalendar();
515 // zero.setTimeInMillis(0);
516 // targetNode.setProperty(Property.JCR_LAST_MODIFIED, zero);
517 // targetNode.getSession().save();
518 // }
519 //
520 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
521 // syncNode_old(it.nextNode(), targetNode);
522 // }
523 //
524 // if (sourceLastModified != null) {
525 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
526 // sourceLastModified);
527 // targetNode.getSession().save();
528 // }
529 // }
530 // }
531
532 protected Boolean singleLevel(Node sourceNode) throws RepositoryException {
533 if (sourceNode.isNodeType(NodeType.NT_FILE))
534 return false;
535 return true;
536 }
537
538 /**
539 * Synchronises only one workspace, retrieved by name without changing its
540 * name.
541 */
542 public void setSourceWksp(String sourceWksp) {
543 if (sourceWksp != null && !sourceWksp.trim().equals("")) {
544 Map<String, String> map = new HashMap<String, String>();
545 map.put(sourceWksp, sourceWksp);
546 setWkspMap(map);
547 }
548 }
549
550 /**
551 * Synchronises a map of workspaces that will be retrieved by name. If the
552 * target name is not defined (eg null or an empty string) for a given
553 * source workspace, we use the source name as target name.
554 */
555 public void setWkspMap(Map<String, String> workspaceMap) {
556 // clean the list to ease later use
557 this.workspaceMap = new HashMap<String, String>();
558 if (workspaceMap != null) {
559 workspaceNames: for (String srcName : workspaceMap.keySet()) {
560 String targetName = workspaceMap.get(srcName);
561
562 // Sanity check
563 if (srcName.trim().equals(""))
564 continue workspaceNames;
565 if (targetName == null || "".equals(targetName.trim()))
566 targetName = srcName;
567 this.workspaceMap.put(srcName, targetName);
568 }
569 }
570 // clean the map to ease later use
571 if (this.workspaceMap.size() == 0)
572 this.workspaceMap = null;
573 }
574
575 public void setMonitor(ArgeoMonitor monitor) {
576 this.monitor = monitor;
577 }
578
579 public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
580 this.repositoryFactory = repositoryFactory;
581 }
582
583 public void setSourceRepoUri(String sourceRepoUri) {
584 this.sourceRepoUri = sourceRepoUri;
585 }
586
587 public void setSourceUsername(String sourceUsername) {
588 this.sourceUsername = sourceUsername;
589 }
590
591 public void setSourcePassword(char[] sourcePassword) {
592 this.sourcePassword = sourcePassword;
593 }
594
595 public void setTargetRepoUri(String targetRepoUri) {
596 this.targetRepoUri = targetRepoUri;
597 }
598
599 public void setTargetUsername(String targetUsername) {
600 this.targetUsername = targetUsername;
601 }
602
603 public void setTargetPassword(char[] targetPassword) {
604 this.targetPassword = targetPassword;
605 }
606
607 public void setSourceRepository(Repository sourceRepository) {
608 this.sourceRepository = sourceRepository;
609 }
610
611 public void setSourceCredentials(Credentials sourceCredentials) {
612 this.sourceCredentials = sourceCredentials;
613 }
614
615 public void setTargetRepository(Repository targetRepository) {
616 this.targetRepository = targetRepository;
617 }
618
619 public void setTargetCredentials(Credentials targetCredentials) {
620 this.targetCredentials = targetCredentials;
621 }
622
623 public void setFilesOnly(Boolean filesOnly) {
624 this.filesOnly = filesOnly;
625 }
626
627 }