View Javadoc
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 }