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.ByteArrayOutputStream;
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.util.Enumeration;
25  import java.util.Iterator;
26  import java.util.Set;
27  import java.util.StringTokenizer;
28  import java.util.TreeSet;
29  import java.util.jar.Attributes;
30  import java.util.jar.JarEntry;
31  import java.util.jar.JarFile;
32  import java.util.jar.JarInputStream;
33  import java.util.jar.JarOutputStream;
34  import java.util.jar.Manifest;
35  import java.util.zip.ZipInputStream;
36  
37  import javax.jcr.Credentials;
38  import javax.jcr.GuestCredentials;
39  import javax.jcr.Node;
40  import javax.jcr.NodeIterator;
41  import javax.jcr.Property;
42  import javax.jcr.PropertyIterator;
43  import javax.jcr.Repository;
44  import javax.jcr.RepositoryException;
45  import javax.jcr.RepositoryFactory;
46  import javax.jcr.Session;
47  import javax.jcr.SimpleCredentials;
48  import javax.jcr.nodetype.NodeType;
49  
50  import org.apache.commons.io.FilenameUtils;
51  import org.apache.commons.io.IOUtils;
52  import org.apache.commons.logging.Log;
53  import org.apache.commons.logging.LogFactory;
54  import org.argeo.cms.ArgeoNames;
55  import org.argeo.cms.ArgeoTypes;
56  import org.argeo.jcr.JcrMonitor;
57  import org.argeo.jcr.JcrUtils;
58  import org.argeo.node.NodeUtils;
59  import org.argeo.node.security.Keyring;
60  import org.argeo.slc.DefaultNameVersion;
61  import org.argeo.slc.NameVersion;
62  import org.argeo.slc.SlcException;
63  import org.argeo.slc.SlcNames;
64  import org.argeo.slc.SlcTypes;
65  import org.argeo.slc.repo.maven.ArtifactIdComparator;
66  import org.argeo.slc.repo.maven.MavenConventionsUtils;
67  import org.eclipse.aether.artifact.Artifact;
68  import org.eclipse.aether.artifact.DefaultArtifact;
69  import org.osgi.framework.Constants;
70  
71  /** Utilities around repo */
72  public class RepoUtils implements ArgeoNames, SlcNames {
73  	private final static Log log = LogFactory.getLog(RepoUtils.class);
74  
75  	/** Packages a regular sources jar as PDE source. */
76  	public static void packagesAsPdeSource(File sourceFile,
77  			NameVersion nameVersion, OutputStream out) throws IOException {
78  		if (isAlreadyPdeSource(sourceFile)) {
79  			FileInputStream in = new FileInputStream(sourceFile);
80  			IOUtils.copy(in, out);
81  			IOUtils.closeQuietly(in);
82  		} else {
83  			String sourceSymbolicName = nameVersion.getName() + ".source";
84  
85  			Manifest sourceManifest = null;
86  			sourceManifest = new Manifest();
87  			sourceManifest.getMainAttributes().put(
88  					Attributes.Name.MANIFEST_VERSION, "1.0");
89  			sourceManifest.getMainAttributes().putValue("Bundle-SymbolicName",
90  					sourceSymbolicName);
91  			sourceManifest.getMainAttributes().putValue("Bundle-Version",
92  					nameVersion.getVersion());
93  			sourceManifest.getMainAttributes().putValue(
94  					"Eclipse-SourceBundle",
95  					nameVersion.getName() + ";version="
96  							+ nameVersion.getVersion());
97  			copyJar(sourceFile, out, sourceManifest);
98  		}
99  	}
100 
101 	public static byte[] packageAsPdeSource(InputStream sourceJar,
102 			NameVersion nameVersion) {
103 		String sourceSymbolicName = nameVersion.getName() + ".source";
104 
105 		Manifest sourceManifest = null;
106 		sourceManifest = new Manifest();
107 		sourceManifest.getMainAttributes().put(
108 				Attributes.Name.MANIFEST_VERSION, "1.0");
109 		sourceManifest.getMainAttributes().putValue("Bundle-SymbolicName",
110 				sourceSymbolicName);
111 		sourceManifest.getMainAttributes().putValue("Bundle-Version",
112 				nameVersion.getVersion());
113 		sourceManifest.getMainAttributes().putValue("Eclipse-SourceBundle",
114 				nameVersion.getName() + ";version=" + nameVersion.getVersion());
115 
116 		return modifyManifest(sourceJar, sourceManifest);
117 	}
118 
119 	/**
120 	 * Check whether the file as already been packaged as PDE source, in order
121 	 * not to mess with Jar signing
122 	 */
123 	private static boolean isAlreadyPdeSource(File sourceFile) {
124 		JarInputStream jarInputStream = null;
125 
126 		try {
127 			jarInputStream = new JarInputStream(new FileInputStream(sourceFile));
128 
129 			Manifest manifest = jarInputStream.getManifest();
130 			Iterator<?> it = manifest.getMainAttributes().keySet().iterator();
131 			boolean res = false;
132 			// containsKey() does not work, iterating...
133 			while (it.hasNext())
134 				if (it.next().toString().equals("Eclipse-SourceBundle")) {
135 					res = true;
136 					break;
137 				}
138 			// boolean res = manifest.getMainAttributes().get(
139 			// "Eclipse-SourceBundle") != null;
140 			if (res)
141 				log.info(sourceFile + " is already a PDE source");
142 			return res;
143 		} catch (Exception e) {
144 			// probably not a jar, skipping
145 			if (log.isDebugEnabled())
146 				log.debug("Skipping " + sourceFile + " because of "
147 						+ e.getMessage());
148 			return false;
149 		} finally {
150 			IOUtils.closeQuietly(jarInputStream);
151 		}
152 	}
153 
154 	/**
155 	 * Copy a jar, replacing its manifest with the provided one
156 	 * 
157 	 * @param manifest
158 	 *            can be null
159 	 */
160 	private static void copyJar(File source, OutputStream out, Manifest manifest)
161 			throws IOException {
162 		JarFile sourceJar = null;
163 		JarOutputStream output = null;
164 		try {
165 			output = manifest != null ? new JarOutputStream(out, manifest)
166 					: new JarOutputStream(out);
167 			sourceJar = new JarFile(source);
168 
169 			entries: for (Enumeration<?> entries = sourceJar.entries(); entries
170 					.hasMoreElements();) {
171 				JarEntry entry = (JarEntry) entries.nextElement();
172 				if (manifest != null
173 						&& entry.getName().equals("META-INF/MANIFEST.MF"))
174 					continue entries;
175 
176 				InputStream entryStream = sourceJar.getInputStream(entry);
177 				JarEntry newEntry = new JarEntry(entry.getName());
178 				// newEntry.setMethod(JarEntry.DEFLATED);
179 				output.putNextEntry(newEntry);
180 				IOUtils.copy(entryStream, output);
181 			}
182 		} finally {
183 			IOUtils.closeQuietly(output);
184 			try {
185 				if (sourceJar != null)
186 					sourceJar.close();
187 			} catch (IOException e) {
188 				// silent
189 			}
190 		}
191 	}
192 
193 	/** Copy a jar changing onlythe manifest */
194 	public static void copyJar(InputStream in, OutputStream out,
195 			Manifest manifest) {
196 		JarInputStream jarIn = null;
197 		JarOutputStream jarOut = null;
198 		try {
199 			jarIn = new JarInputStream(in);
200 			jarOut = new JarOutputStream(out, manifest);
201 			JarEntry jarEntry = null;
202 			while ((jarEntry = jarIn.getNextJarEntry()) != null) {
203 				if (!jarEntry.getName().equals("META-INF/MANIFEST.MF")) {
204 					JarEntry newJarEntry = new JarEntry(jarEntry.getName());
205 					jarOut.putNextEntry(newJarEntry);
206 					IOUtils.copy(jarIn, jarOut);
207 					jarIn.closeEntry();
208 					jarOut.closeEntry();
209 				}
210 			}
211 		} catch (IOException e) {
212 			throw new SlcException("Could not copy jar with MANIFEST "
213 					+ manifest.getMainAttributes(), e);
214 		} finally {
215 			if (!(in instanceof ZipInputStream))
216 				IOUtils.closeQuietly(jarIn);
217 			IOUtils.closeQuietly(jarOut);
218 		}
219 	}
220 
221 	/** Reads a jar file, modify its manifest */
222 	public static byte[] modifyManifest(InputStream in, Manifest manifest) {
223 		ByteArrayOutputStream out = new ByteArrayOutputStream(200 * 1024);
224 		try {
225 			copyJar(in, out, manifest);
226 			return out.toByteArray();
227 		} finally {
228 			IOUtils.closeQuietly(out);
229 		}
230 	}
231 
232 	/** Read the OSGi {@link NameVersion} */
233 	public static NameVersion readNameVersion(Artifact artifact) {
234 		File artifactFile = artifact.getFile();
235 		if (artifact.getExtension().equals("pom")) {
236 			// hack to process jars which weirdly appear as POMs
237 			File jarFile = new File(artifactFile.getParentFile(),
238 					FilenameUtils.getBaseName(artifactFile.getPath()) + ".jar");
239 			if (jarFile.exists()) {
240 				log.warn("Use " + jarFile + " instead of " + artifactFile
241 						+ " for " + artifact);
242 				artifactFile = jarFile;
243 			}
244 		}
245 		return readNameVersion(artifactFile);
246 	}
247 
248 	/** Read the OSGi {@link NameVersion} */
249 	public static NameVersion readNameVersion(File artifactFile) {
250 		try {
251 			return readNameVersion(new FileInputStream(artifactFile));
252 		} catch (Exception e) {
253 			// probably not a jar, skipping
254 			if (log.isDebugEnabled()) {
255 				log.debug("Skipping " + artifactFile + " because of " + e);
256 				// e.printStackTrace();
257 			}
258 		}
259 		return null;
260 	}
261 
262 	/** Read the OSGi {@link NameVersion} */
263 	public static NameVersion readNameVersion(InputStream in) {
264 		JarInputStream jarInputStream = null;
265 		try {
266 			jarInputStream = new JarInputStream(in);
267 			return readNameVersion(jarInputStream.getManifest());
268 		} catch (Exception e) {
269 			// probably not a jar, skipping
270 			if (log.isDebugEnabled()) {
271 				log.debug("Skipping because of " + e);
272 			}
273 		} finally {
274 			IOUtils.closeQuietly(jarInputStream);
275 		}
276 		return null;
277 	}
278 
279 	/** Read the OSGi {@link NameVersion} */
280 	public static NameVersion readNameVersion(Manifest manifest) {
281 		DefaultNameVersion nameVersion = new DefaultNameVersion();
282 		nameVersion.setName(manifest.getMainAttributes().getValue(
283 				Constants.BUNDLE_SYMBOLICNAME));
284 
285 		// Skip additional specs such as
286 		// ; singleton:=true
287 		if (nameVersion.getName().indexOf(';') > -1) {
288 			nameVersion
289 					.setName(new StringTokenizer(nameVersion.getName(), " ;")
290 							.nextToken());
291 		}
292 
293 		nameVersion.setVersion(manifest.getMainAttributes().getValue(
294 				Constants.BUNDLE_VERSION));
295 
296 		return nameVersion;
297 	}
298 
299 	/*
300 	 * DATA MODEL
301 	 */
302 	/** The artifact described by this node */
303 	public static Artifact asArtifact(Node node) throws RepositoryException {
304 		if (node.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
305 			// FIXME update data model to store packaging at this level
306 			String extension = "jar";
307 			return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
308 					.getString(),
309 					node.getProperty(SLC_ARTIFACT_ID).getString(), extension,
310 					node.getProperty(SLC_ARTIFACT_VERSION).getString());
311 		} else if (node.isNodeType(SlcTypes.SLC_ARTIFACT)) {
312 			return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
313 					.getString(),
314 					node.getProperty(SLC_ARTIFACT_ID).getString(), node
315 							.getProperty(SLC_ARTIFACT_CLASSIFIER).getString(),
316 					node.getProperty(SLC_ARTIFACT_EXTENSION).getString(), node
317 							.getProperty(SLC_ARTIFACT_VERSION).getString());
318 		} else if (node.isNodeType(SlcTypes.SLC_MODULE_COORDINATES)) {
319 			return new DefaultArtifact(node.getProperty(SLC_CATEGORY)
320 					.getString(), node.getProperty(SLC_NAME).getString(),
321 					"jar", node.getProperty(SLC_VERSION).getString());
322 		} else {
323 			throw new SlcException("Unsupported node type for " + node);
324 		}
325 	}
326 
327 	/**
328 	 * The path to the PDE source related to this artifact (or artifact version
329 	 * base). There may or there may not be a node at this location (the
330 	 * returned path will typically be used to test whether PDE sources are
331 	 * attached to this artifact).
332 	 */
333 	public static String relatedPdeSourcePath(String artifactBasePath,
334 			Node artifactNode) throws RepositoryException {
335 		Artifact artifact = asArtifact(artifactNode);
336 		Artifact pdeSourceArtifact = new DefaultArtifact(artifact.getGroupId(),
337 				artifact.getArtifactId() + ".source", artifact.getExtension(),
338 				artifact.getVersion());
339 		return MavenConventionsUtils.artifactPath(artifactBasePath,
340 				pdeSourceArtifact);
341 	}
342 
343 	/**
344 	 * Copy this bytes array as an artifact, relative to the root of the
345 	 * repository (typically the workspace root node)
346 	 */
347 	public static Node copyBytesAsArtifact(Node artifactsBase,
348 			Artifact artifact, byte[] bytes) throws RepositoryException {
349 		String parentPath = MavenConventionsUtils.artifactParentPath(
350 				artifactsBase.getPath(), artifact);
351 		Node folderNode = JcrUtils.mkfolders(artifactsBase.getSession(),
352 				parentPath);
353 		return JcrUtils.copyBytesAsFile(folderNode,
354 				MavenConventionsUtils.artifactFileName(artifact), bytes);
355 	}
356 
357 	private RepoUtils() {
358 	}
359 
360 	/** If a source return the base bundle name, does not change otherwise */
361 	public static String extractBundleNameFromSourceName(String sourceBundleName) {
362 		if (sourceBundleName.endsWith(".source"))
363 			return sourceBundleName.substring(0, sourceBundleName.length()
364 					- ".source".length());
365 		else
366 			return sourceBundleName;
367 	}
368 
369 	/*
370 	 * SOFTWARE REPOSITORIES
371 	 */
372 
373 	/** Retrieve repository based on information in the repo node */
374 	public static Repository getRepository(RepositoryFactory repositoryFactory,
375 			Keyring keyring, Node repoNode) {
376 		try {
377 			Repository repository;
378 			if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
379 				String uri = repoNode.getProperty(ARGEO_URI).getString();
380 				if (uri.startsWith("http")) {// http, https
381 					repository = NodeUtils.getRepositoryByUri(
382 							repositoryFactory, uri);
383 				} else if (uri.startsWith("vm:")) {// alias
384 					repository = NodeUtils.getRepositoryByUri(
385 							repositoryFactory, uri);
386 				} else {
387 					throw new SlcException("Unsupported repository uri " + uri);
388 				}
389 				return repository;
390 			} else {
391 				throw new SlcException("Unsupported node type " + repoNode);
392 			}
393 		} catch (RepositoryException e) {
394 			throw new SlcException("Cannot connect to repository " + repoNode,
395 					e);
396 		}
397 	}
398 
399 	/**
400 	 * Reads credentials from node, using keyring if there is a password. Can
401 	 * return null if no credentials needed (local repo) at all, but returns
402 	 * {@link GuestCredentials} if user id is 'anonymous' .
403 	 */
404 	public static Credentials getRepositoryCredentials(Keyring keyring,
405 			Node repoNode) {
406 		try {
407 			if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
408 				if (!repoNode.hasProperty(ARGEO_USER_ID))
409 					return null;
410 
411 				String userId = repoNode.getProperty(ARGEO_USER_ID).getString();
412 				if (userId.equals("anonymous"))// FIXME hardcoded userId
413 					return new GuestCredentials();
414 				char[] password = keyring.getAsChars(repoNode.getPath() + '/'
415 						+ ARGEO_PASSWORD);
416 				Credentials credentials = new SimpleCredentials(userId,
417 						password);
418 				return credentials;
419 			} else {
420 				throw new SlcException("Unsupported node type " + repoNode);
421 			}
422 		} catch (RepositoryException e) {
423 			throw new SlcException("Cannot connect to repository " + repoNode,
424 					e);
425 		}
426 	}
427 
428 	/**
429 	 * Shortcut to retrieve a session given variable information: Handle the
430 	 * case where we only have an URI of the repository, that we want to connect
431 	 * as anonymous or the case of a identified connection to a local or remote
432 	 * repository.
433 	 * 
434 	 * Callers must close the session once it has been used
435 	 */
436 	public static Session getRemoteSession(RepositoryFactory repositoryFactory,
437 			Keyring keyring, Node repoNode, String uri, String workspaceName) {
438 		try {
439 			if (repoNode == null && uri == null)
440 				throw new SlcException(
441 						"At least one of repoNode and uri must be defined");
442 			Repository currRepo = null;
443 			Credentials credentials = null;
444 			// Anonymous URI only workspace
445 			if (repoNode == null)
446 				// Anonymous
447 				currRepo = NodeUtils.getRepositoryByUri(repositoryFactory, uri);
448 			else {
449 				currRepo = RepoUtils.getRepository(repositoryFactory, keyring,
450 						repoNode);
451 				credentials = RepoUtils.getRepositoryCredentials(keyring,
452 						repoNode);
453 			}
454 			return currRepo.login(credentials, workspaceName);
455 		} catch (RepositoryException e) {
456 			throw new SlcException("Cannot connect to workspace "
457 					+ workspaceName + " of repository " + repoNode
458 					+ " with URI " + uri, e);
459 		}
460 	}
461 
462 	/**
463 	 * Shortcut to retrieve a session on a remote Jrc Repository from
464 	 * information stored in a local argeo node or from an URI: Handle the case
465 	 * where we only have an URI of the repository, that we want to connect as
466 	 * anonymous or the case of a identified connection to a local or remote
467 	 * repository.
468 	 * 
469 	 * Callers must close the session once it has been used
470 	 */
471 	public static Session getRemoteSession(RepositoryFactory repositoryFactory,
472 			Keyring keyring, Repository localRepository, String repoNodePath,
473 			String uri, String workspaceName) {
474 		Session localSession = null;
475 		Node repoNode = null;
476 		try {
477 			localSession = localRepository.login();
478 			if (repoNodePath != null && localSession.nodeExists(repoNodePath))
479 				repoNode = localSession.getNode(repoNodePath);
480 
481 			return RepoUtils.getRemoteSession(repositoryFactory, keyring,
482 					repoNode, uri, workspaceName);
483 		} catch (RepositoryException e) {
484 			throw new SlcException("Cannot log to workspace " + workspaceName
485 					+ " for repo defined in " + repoNodePath, e);
486 		} finally {
487 			JcrUtils.logoutQuietly(localSession);
488 		}
489 	}
490 
491 	/**
492 	 * Write group indexes: 'binaries' lists all bundles and their versions,
493 	 * 'sources' list their sources, and 'sdk' aggregates both.
494 	 */
495 	public static void writeGroupIndexes(Session session,
496 			String artifactBasePath, String groupId, String version,
497 			Set<Artifact> binaries, Set<Artifact> sources) {
498 		try {
499 			Set<Artifact> indexes = new TreeSet<Artifact>(
500 					new ArtifactIdComparator());
501 			Artifact binariesArtifact = writeIndex(session, artifactBasePath,
502 					groupId, RepoConstants.BINARIES_ARTIFACT_ID, version,
503 					binaries);
504 			indexes.add(binariesArtifact);
505 			if (sources != null) {
506 				Artifact sourcesArtifact = writeIndex(session,
507 						artifactBasePath, groupId,
508 						RepoConstants.SOURCES_ARTIFACT_ID, version, sources);
509 				indexes.add(sourcesArtifact);
510 			}
511 			// sdk
512 			writeIndex(session, artifactBasePath, groupId,
513 					RepoConstants.SDK_ARTIFACT_ID, version, indexes);
514 			session.save();
515 		} catch (RepositoryException e) {
516 			throw new SlcException("Cannot write indexes for group " + groupId,
517 					e);
518 		}
519 	}
520 
521 	/** Write a group index. */
522 	private static Artifact writeIndex(Session session,
523 			String artifactBasePath, String groupId, String artifactId,
524 			String version, Set<Artifact> artifacts) throws RepositoryException {
525 		Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom",
526 				version);
527 		String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact,
528 				artifacts, null);
529 		Node node = RepoUtils.copyBytesAsArtifact(
530 				session.getNode(artifactBasePath), artifact, pom.getBytes());
531 		addMavenChecksums(node);
532 		return artifact;
533 	}
534 
535 	/** Add files containing the SHA-1 and MD5 checksums. */
536 	public static void addMavenChecksums(Node node) throws RepositoryException {
537 		// TODO optimize
538 		String sha = JcrUtils.checksumFile(node, "SHA-1");
539 		JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1",
540 				sha.getBytes());
541 		String md5 = JcrUtils.checksumFile(node, "MD5");
542 		JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5",
543 				md5.getBytes());
544 	}
545 
546 	/**
547 	 * Custom copy since the one in commons does not fit the needs when copying
548 	 * a workspace completely.
549 	 */
550 	public static void copy(Node fromNode, Node toNode) {
551 		copy(fromNode, toNode, null);
552 	}
553 
554 	public static void copy(Node fromNode, Node toNode, JcrMonitor monitor) {
555 		try {
556 			String fromPath = fromNode.getPath();
557 			if (monitor != null)
558 				monitor.subTask("copying node :" + fromPath);
559 			if (log.isDebugEnabled())
560 				log.debug("copy node :" + fromPath);
561 
562 			// FIXME : small hack to enable specific workspace copy
563 			if (fromNode.isNodeType("rep:ACL")
564 					|| fromNode.isNodeType("rep:system")) {
565 				if (log.isTraceEnabled())
566 					log.trace("node " + fromNode + " skipped");
567 				return;
568 			}
569 
570 			// add mixins
571 			for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
572 				toNode.addMixin(mixinType.getName());
573 			}
574 
575 			// Double check
576 			for (NodeType mixinType : toNode.getMixinNodeTypes()) {
577 				if (log.isDebugEnabled())
578 					log.debug(mixinType.getName());
579 			}
580 
581 			// process properties
582 			PropertyIterator pit = fromNode.getProperties();
583 			properties: while (pit.hasNext()) {
584 				Property fromProperty = pit.nextProperty();
585 				String propName = fromProperty.getName();
586 				try {
587 					String propertyName = fromProperty.getName();
588 					if (toNode.hasProperty(propertyName)
589 							&& toNode.getProperty(propertyName).getDefinition()
590 									.isProtected())
591 						continue properties;
592 
593 					if (fromProperty.getDefinition().isProtected())
594 						continue properties;
595 
596 					if (propertyName.equals("jcr:created")
597 							|| propertyName.equals("jcr:createdBy")
598 							|| propertyName.equals("jcr:lastModified")
599 							|| propertyName.equals("jcr:lastModifiedBy"))
600 						continue properties;
601 
602 					if (fromProperty.isMultiple()) {
603 						toNode.setProperty(propertyName,
604 								fromProperty.getValues());
605 					} else {
606 						toNode.setProperty(propertyName,
607 								fromProperty.getValue());
608 					}
609 				} catch (RepositoryException e) {
610 					throw new SlcException("Cannot property " + propName, e);
611 				}
612 			}
613 
614 			// recursively process children nodes
615 			NodeIterator nit = fromNode.getNodes();
616 			while (nit.hasNext()) {
617 				Node fromChild = nit.nextNode();
618 				Integer index = fromChild.getIndex();
619 				String nodeRelPath = fromChild.getName() + "[" + index + "]";
620 				Node toChild;
621 				if (toNode.hasNode(nodeRelPath))
622 					toChild = toNode.getNode(nodeRelPath);
623 				else
624 					toChild = toNode.addNode(fromChild.getName(), fromChild
625 							.getPrimaryNodeType().getName());
626 				copy(fromChild, toChild);
627 			}
628 
629 			// update jcr:lastModified and jcr:lastModifiedBy in toNode in
630 			// case
631 			// they existed
632 			if (!toNode.getDefinition().isProtected()
633 					&& toNode.isNodeType(NodeType.MIX_LAST_MODIFIED))
634 				JcrUtils.updateLastModified(toNode);
635 
636 			// Workaround to reduce session size: artifact is a saveable
637 			// unity
638 			if (toNode.isNodeType(SlcTypes.SLC_ARTIFACT))
639 				toNode.getSession().save();
640 
641 			if (monitor != null)
642 				monitor.worked(1);
643 
644 		} catch (RepositoryException e) {
645 			throw new SlcException("Cannot copy " + fromNode + " to " + toNode,
646 					e);
647 		}
648 	}
649 
650 }