]> git.argeo.org Git - gpl/argeo-slc.git/blob - runtime/org.argeo.slc.repo/src/main/java/org/argeo/slc/repo/RepoSync.java
Clean session management.
[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 e.printStackTrace();
252 throw new SlcException("Cannot sync "
253 + sourceSession.getWorkspace().getName() + " to "
254 + targetSession.getWorkspace().getName(), e);
255 }
256 }
257
258 /** factorizes monitor management */
259 private void updateMonitor(String msg) {
260 updateMonitor(msg, false);
261 }
262
263 protected void syncNode(Node sourceNode, Session targetSession)
264 throws RepositoryException, SAXException {
265 // Boolean singleLevel = singleLevel(sourceNode);
266 try {
267 if (monitor != null && monitor.isCanceled()) {
268 updateMonitor("Fetched has been canceled, "
269 + "process is terminating");
270 return;
271 }
272
273 Node targetParentNode = targetSession.getNode(sourceNode
274 .getParent().getPath());
275 Node targetNode;
276 if (monitor != null
277 && sourceNode.isNodeType(NodeType.NT_HIERARCHY_NODE))
278 monitor.subTask("Process " + sourceNode.getPath());
279
280 final Boolean isNew;
281 if (!targetSession.itemExists(sourceNode.getPath())) {
282 isNew = true;
283 targetNode = targetParentNode.addNode(sourceNode.getName(),
284 sourceNode.getPrimaryNodeType().getName());
285 } else {
286 isNew = false;
287 targetNode = targetSession.getNode(sourceNode.getPath());
288 if (!targetNode.getPrimaryNodeType().getName()
289 .equals(sourceNode.getPrimaryNodeType().getName()))
290 targetNode.setPrimaryType(sourceNode.getPrimaryNodeType()
291 .getName());
292 }
293
294 // export
295 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
296 // contentHandler, false, singleLevel);
297
298 // if (singleLevel) {
299 // if (targetSession.hasPendingChanges()) {
300 // // updateMonitor(
301 // // (isNew ? "Added " : "Updated ") + targetNode.getPath(),
302 // // true);
303 // if (doSave)
304 // targetSession.save();
305 // } else {
306 // // updateMonitor("Checked " + targetNode.getPath(), false);
307 // }
308 // }
309
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());
315 }
316 copyProperties(sourceNode, targetNode);
317
318 // next level
319 NodeIterator ni = sourceNode.getNodes();
320 while (ni != null && ni.hasNext()) {
321 Node sourceChild = ni.nextNode();
322 syncNode(sourceChild, targetSession);
323 }
324
325 copyTimestamps(sourceNode, targetNode);
326
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);
332 // if (doSave)
333 targetSession.save();
334 } else {
335 if (sourceNode.isNodeType(NodeType.NT_FILE))
336 updateMonitor("Checked " + targetNode.getPath(), false);
337 }
338 }
339 } catch (RepositoryException e) {
340 throw new SlcException("Cannot sync source node " + sourceNode, e);
341 }
342 }
343
344 private void copyTimestamps(Node sourceNode, Node targetNode)
345 throws RepositoryException {
346 if (sourceNode.getDefinition().isProtected())
347 return;
348 if (targetNode.getDefinition().isProtected())
349 return;
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);
354 }
355
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())
361 return;
362 if (targetNode.hasProperty(property)
363 && targetNode
364 .getProperty(property)
365 .getValue()
366 .equals(sourceNode.getProperty(property).getValue()))
367 return;
368 targetNode.setProperty(property, sourceNode.getProperty(property)
369 .getValue());
370 }
371 }
372
373 private void copyProperties(Node sourceNode, Node targetNode)
374 throws RepositoryException {
375 properties: for (PropertyIterator pi = sourceNode.getProperties(); pi
376 .hasNext();) {
377 Property p = pi.nextProperty();
378 if (p.getDefinition().isProtected())
379 continue properties;
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))
384 continue properties;
385
386 if (p.getType() == PropertyType.BINARY) {
387 copyBinary(p, targetNode);
388 } else {
389
390 if (p.isMultiple()) {
391 if (!targetNode.hasProperty(p.getName())
392 || !Arrays.equals(
393 targetNode.getProperty(p.getName())
394 .getValues(), p.getValues()))
395 targetNode.setProperty(p.getName(), p.getValues());
396 } else {
397 if (!targetNode.hasProperty(p.getName())
398 || !targetNode.getProperty(p.getName()).getValue()
399 .equals(p.getValue()))
400 targetNode.setProperty(p.getName(), p.getValue());
401 }
402 }
403 }
404 }
405
406 private static void copyBinary(Property p, Node targetNode)
407 throws RepositoryException {
408 InputStream in = null;
409 Binary sourceBinary = null;
410 Binary targetBinary = null;
411 try {
412 sourceBinary = p.getBinary();
413 if (targetNode.hasProperty(p.getName()))
414 targetBinary = targetNode.getProperty(p.getName()).getBinary();
415
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());
421 return;
422 }
423
424 in = sourceBinary.getStream();
425 targetBinary = targetNode.getSession().getValueFactory()
426 .createBinary(in);
427 targetNode.setProperty(p.getName(), targetBinary);
428 } catch (Exception e) {
429 throw new SlcException("Could not transfer " + p, e);
430 } finally {
431 IOUtils.closeQuietly(in);
432 JcrUtils.closeQuietly(sourceBinary);
433 JcrUtils.closeQuietly(targetBinary);
434 }
435 }
436
437 /** factorizes monitor management */
438 private void updateMonitor(String msg, Boolean doLog) {
439 if (doLog && log.isDebugEnabled())
440 log.debug(msg);
441 if (monitor != null) {
442 monitor.worked(1);
443 monitor.subTask(msg);
444 }
445 }
446
447 // private void syncNode_old(Node sourceNode, Node targetParentNode)
448 // throws RepositoryException, SAXException {
449 //
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");
455 // return;
456 // }
457 //
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();
463 // }
464 //
465 // if (sourceNode.getDefinition().isProtected())
466 // log.warn(sourceNode + " is protected.");
467 //
468 // if (!targetParentNode.hasNode(sourceNode.getName())) {
469 // String msg = "Adding " + sourceNode.getPath();
470 // updateMonitor(msg);
471 // if (log.isDebugEnabled())
472 // log.debug(msg);
473 // ContentHandler contentHandler = targetParentNode
474 // .getSession()
475 // .getWorkspace()
476 // .getImportContentHandler(targetParentNode.getPath(),
477 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
478 // sourceNode.getSession().exportSystemView(sourceNode.getPath(),
479 // contentHandler, false, noRecurse);
480 // } else {
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();
487 // }
488 //
489 // if (targetLastModified == null
490 // || targetLastModified.before(sourceLastModified)) {
491 // String msg = "Updating " + targetNode.getPath();
492 // updateMonitor(msg);
493 // if (log.isDebugEnabled())
494 // log.debug(msg);
495 // ContentHandler contentHandler = targetParentNode
496 // .getSession()
497 // .getWorkspace()
498 // .getImportContentHandler(
499 // targetParentNode.getPath(),
500 // ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
501 // sourceNode.getSession().exportSystemView(
502 // sourceNode.getPath(), contentHandler, false,
503 // noRecurse);
504 // } else {
505 // String msg = "Skipped up to date " + targetNode.getPath();
506 // updateMonitor(msg);
507 // if (log.isDebugEnabled())
508 // log.debug(msg);
509 // return;
510 // }
511 // }
512 // }
513 //
514 // if (noRecurse) {
515 // // recurse
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();
522 // }
523 //
524 // for (NodeIterator it = sourceNode.getNodes(); it.hasNext();) {
525 // syncNode_old(it.nextNode(), targetNode);
526 // }
527 //
528 // if (sourceLastModified != null) {
529 // targetNode.setProperty(Property.JCR_LAST_MODIFIED,
530 // sourceLastModified);
531 // targetNode.getSession().save();
532 // }
533 // }
534 // }
535
536 protected Boolean singleLevel(Node sourceNode) throws RepositoryException {
537 if (sourceNode.isNodeType(NodeType.NT_FILE))
538 return false;
539 return true;
540 }
541
542 /**
543 * Synchronises only one workspace, retrieved by name without changing its
544 * name.
545 */
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);
550 setWkspMap(map);
551 }
552 }
553
554 /**
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.
558 */
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);
565
566 // Sanity check
567 if (srcName.trim().equals(""))
568 continue workspaceNames;
569 if (targetName == null || "".equals(targetName.trim()))
570 targetName = srcName;
571 this.workspaceMap.put(srcName, targetName);
572 }
573 }
574 // clean the map to ease later use
575 if (this.workspaceMap.size() == 0)
576 this.workspaceMap = null;
577 }
578
579 public void setMonitor(ArgeoMonitor monitor) {
580 this.monitor = monitor;
581 }
582
583 public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
584 this.repositoryFactory = repositoryFactory;
585 }
586
587 public void setSourceRepoUri(String sourceRepoUri) {
588 this.sourceRepoUri = sourceRepoUri;
589 }
590
591 public void setSourceUsername(String sourceUsername) {
592 this.sourceUsername = sourceUsername;
593 }
594
595 public void setSourcePassword(char[] sourcePassword) {
596 this.sourcePassword = sourcePassword;
597 }
598
599 public void setTargetRepoUri(String targetRepoUri) {
600 this.targetRepoUri = targetRepoUri;
601 }
602
603 public void setTargetUsername(String targetUsername) {
604 this.targetUsername = targetUsername;
605 }
606
607 public void setTargetPassword(char[] targetPassword) {
608 this.targetPassword = targetPassword;
609 }
610
611 public void setSourceRepository(Repository sourceRepository) {
612 this.sourceRepository = sourceRepository;
613 }
614
615 public void setSourceCredentials(Credentials sourceCredentials) {
616 this.sourceCredentials = sourceCredentials;
617 }
618
619 public void setTargetRepository(Repository targetRepository) {
620 this.targetRepository = targetRepository;
621 }
622
623 public void setTargetCredentials(Credentials targetCredentials) {
624 this.targetCredentials = targetCredentials;
625 }
626
627 public void setFilesOnly(Boolean filesOnly) {
628 this.filesOnly = filesOnly;
629 }
630
631 }