]> git.argeo.org Git - gpl/argeo-slc.git/blob - runtime/org.argeo.slc.repo/src/main/java/org/argeo/slc/repo/RepoUtils.java
4a175ac594ce641569e269bd37c6c201d753e5a3
[gpl/argeo-slc.git] / runtime / org.argeo.slc.repo / src / main / java / org / argeo / slc / repo / RepoUtils.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.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.ArgeoMonitor;
55 import org.argeo.jcr.ArgeoJcrUtils;
56 import org.argeo.jcr.ArgeoNames;
57 import org.argeo.jcr.ArgeoTypes;
58 import org.argeo.jcr.JcrUtils;
59 import org.argeo.slc.DefaultNameVersion;
60 import org.argeo.slc.NameVersion;
61 import org.argeo.slc.SlcException;
62 import org.argeo.slc.aether.ArtifactIdComparator;
63 import org.argeo.slc.jcr.SlcNames;
64 import org.argeo.slc.jcr.SlcTypes;
65 import org.argeo.slc.repo.maven.MavenConventionsUtils;
66 import org.argeo.util.security.Keyring;
67 import org.osgi.framework.Constants;
68 import org.sonatype.aether.artifact.Artifact;
69 import org.sonatype.aether.util.artifact.DefaultArtifact;
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 e.printStackTrace();
273 }
274 } finally {
275 IOUtils.closeQuietly(jarInputStream);
276 }
277 return null;
278 }
279
280 /** Read the OSGi {@link NameVersion} */
281 public static NameVersion readNameVersion(Manifest manifest) {
282 DefaultNameVersion nameVersion = new DefaultNameVersion();
283 nameVersion.setName(manifest.getMainAttributes().getValue(
284 Constants.BUNDLE_SYMBOLICNAME));
285
286 // Skip additional specs such as
287 // ; singleton:=true
288 if (nameVersion.getName().indexOf(';') > -1) {
289 nameVersion
290 .setName(new StringTokenizer(nameVersion.getName(), " ;")
291 .nextToken());
292 }
293
294 nameVersion.setVersion(manifest.getMainAttributes().getValue(
295 Constants.BUNDLE_VERSION));
296
297 return nameVersion;
298 }
299
300 /*
301 * DATA MODEL
302 */
303 /** The artifact described by this node */
304 public static Artifact asArtifact(Node node) throws RepositoryException {
305 if (node.isNodeType(SlcTypes.SLC_ARTIFACT_VERSION_BASE)) {
306 // FIXME update data model to store packaging at this level
307 String extension = "jar";
308 return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
309 .getString(),
310 node.getProperty(SLC_ARTIFACT_ID).getString(), extension,
311 node.getProperty(SLC_ARTIFACT_VERSION).getString());
312 } else if (node.isNodeType(SlcTypes.SLC_ARTIFACT)) {
313 return new DefaultArtifact(node.getProperty(SLC_GROUP_ID)
314 .getString(),
315 node.getProperty(SLC_ARTIFACT_ID).getString(), node
316 .getProperty(SLC_ARTIFACT_CLASSIFIER).getString(),
317 node.getProperty(SLC_ARTIFACT_EXTENSION).getString(), node
318 .getProperty(SLC_ARTIFACT_VERSION).getString());
319 } else if (node.isNodeType(SlcTypes.SLC_MODULE_COORDINATES)) {
320 return new DefaultArtifact(node.getProperty(SLC_CATEGORY)
321 .getString(), node.getProperty(SLC_NAME).getString(),
322 "jar", node.getProperty(SLC_VERSION).getString());
323 } else {
324 throw new SlcException("Unsupported node type for " + node);
325 }
326 }
327
328 /**
329 * The path to the PDE source related to this artifact (or artifact version
330 * base). There may or there may not be a node at this location (the
331 * returned path will typically be used to test whether PDE sources are
332 * attached to this artifact).
333 */
334 public static String relatedPdeSourcePath(String artifactBasePath,
335 Node artifactNode) throws RepositoryException {
336 Artifact artifact = asArtifact(artifactNode);
337 Artifact pdeSourceArtifact = new DefaultArtifact(artifact.getGroupId(),
338 artifact.getArtifactId() + ".source", artifact.getExtension(),
339 artifact.getVersion());
340 return MavenConventionsUtils.artifactPath(artifactBasePath,
341 pdeSourceArtifact);
342 }
343
344 /**
345 * Copy this bytes array as an artifact, relative to the root of the
346 * repository (typically the workspace root node)
347 */
348 public static Node copyBytesAsArtifact(Node artifactsBase,
349 Artifact artifact, byte[] bytes) throws RepositoryException {
350 String parentPath = MavenConventionsUtils.artifactParentPath(
351 artifactsBase.getPath(), artifact);
352 Node folderNode = JcrUtils.mkfolders(artifactsBase.getSession(),
353 parentPath);
354 return JcrUtils.copyBytesAsFile(folderNode,
355 MavenConventionsUtils.artifactFileName(artifact), bytes);
356 }
357
358 private RepoUtils() {
359 }
360
361 /** If a source return the base bundle name, does not change otherwise */
362 public static String extractBundleNameFromSourceName(String sourceBundleName) {
363 if (sourceBundleName.endsWith(".source"))
364 return sourceBundleName.substring(0, sourceBundleName.length()
365 - ".source".length());
366 else
367 return sourceBundleName;
368 }
369
370 /*
371 * SOFTWARE REPOSITORIES
372 */
373
374 /** Retrieve repository based on information in the repo node */
375 public static Repository getRepository(RepositoryFactory repositoryFactory,
376 Keyring keyring, Node repoNode) {
377 try {
378 Repository repository;
379 if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
380 String uri = repoNode.getProperty(ARGEO_URI).getString();
381 if (uri.startsWith("http")) {// http, https
382 repository = ArgeoJcrUtils.getRepositoryByUri(
383 repositoryFactory, uri);
384 } else if (uri.startsWith("vm:")) {// alias
385 repository = ArgeoJcrUtils.getRepositoryByUri(
386 repositoryFactory, uri);
387 } else {
388 throw new SlcException("Unsupported repository uri " + uri);
389 }
390 return repository;
391 } else {
392 throw new SlcException("Unsupported node type " + repoNode);
393 }
394 } catch (RepositoryException e) {
395 throw new SlcException("Cannot connect to repository " + repoNode,
396 e);
397 }
398 }
399
400 /**
401 * Reads credentials from node, using keyring if there is a password. Can
402 * return null if no credentials needed (local repo) at all, but returns
403 * {@link GuestCredentials} if user id is 'anonymous' .
404 */
405 public static Credentials getRepositoryCredentials(Keyring keyring,
406 Node repoNode) {
407 try {
408 if (repoNode.isNodeType(ArgeoTypes.ARGEO_REMOTE_REPOSITORY)) {
409 if (!repoNode.hasProperty(ARGEO_USER_ID))
410 return null;
411
412 String userId = repoNode.getProperty(ARGEO_USER_ID).getString();
413 if (userId.equals("anonymous"))// FIXME hardcoded userId
414 return new GuestCredentials();
415 char[] password = keyring.getAsChars(repoNode.getPath() + '/'
416 + ARGEO_PASSWORD);
417 Credentials credentials = new SimpleCredentials(userId,
418 password);
419 return credentials;
420 } else {
421 throw new SlcException("Unsupported node type " + repoNode);
422 }
423 } catch (RepositoryException e) {
424 throw new SlcException("Cannot connect to repository " + repoNode,
425 e);
426 }
427 }
428
429 /**
430 * Shortcut to retrieve a session given variable information: Handle the
431 * case where we only have an URI of the repository, that we want to connect
432 * as anonymous or the case of a identified connection to a local or remote
433 * repository.
434 *
435 * Callers must close the session once it has been used
436 */
437 public static Session getRemoteSession(RepositoryFactory repositoryFactory,
438 Keyring keyring, Node repoNode, String uri, String workspaceName) {
439 try {
440 if (repoNode == null && uri == null)
441 throw new SlcException(
442 "At least one of repoNode and uri must be defined");
443 Repository currRepo = null;
444 Credentials credentials = null;
445 // Anonymous URI only workspace
446 if (repoNode == null)
447 // Anonymous
448 currRepo = ArgeoJcrUtils.getRepositoryByUri(repositoryFactory,
449 uri);
450 else {
451 currRepo = RepoUtils.getRepository(repositoryFactory, keyring,
452 repoNode);
453 credentials = RepoUtils.getRepositoryCredentials(keyring,
454 repoNode);
455 }
456 return currRepo.login(credentials, workspaceName);
457 } catch (RepositoryException e) {
458 throw new SlcException("Cannot connect to workspace "
459 + workspaceName + " of repository " + repoNode
460 + " with URI " + uri, e);
461 }
462 }
463
464 /**
465 * Shortcut to retrieve a session on a remote Jrc Repository from
466 * information stored in a local argeo node or from an URI: Handle the case
467 * where we only have an URI of the repository, that we want to connect as
468 * anonymous or the case of a identified connection to a local or remote
469 * repository.
470 *
471 * Callers must close the session once it has been used
472 */
473 public static Session getRemoteSession(RepositoryFactory repositoryFactory,
474 Keyring keyring, Repository localRepository, String repoNodePath,
475 String uri, String workspaceName) {
476 Session localSession = null;
477 Node repoNode = null;
478 try {
479 localSession = localRepository.login();
480 if (repoNodePath != null && localSession.nodeExists(repoNodePath))
481 repoNode = localSession.getNode(repoNodePath);
482
483 return RepoUtils.getRemoteSession(repositoryFactory, keyring,
484 repoNode, uri, workspaceName);
485 } catch (RepositoryException e) {
486 throw new SlcException("Cannot log to workspace " + workspaceName
487 + " for repo defined in " + repoNodePath, e);
488 } finally {
489 JcrUtils.logoutQuietly(localSession);
490 }
491 }
492
493 /**
494 * Write group indexes: 'binaries' lists all bundles and their versions,
495 * 'sources' list their sources, and 'sdk' aggregates both.
496 */
497 public static void writeGroupIndexes(Session session,
498 String artifactBasePath, String groupId, String version,
499 Set<Artifact> binaries, Set<Artifact> sources) {
500 try {
501 Set<Artifact> indexes = new TreeSet<Artifact>(
502 new ArtifactIdComparator());
503 Artifact binariesArtifact = writeIndex(session, artifactBasePath,
504 groupId, RepoConstants.BINARIES_ARTIFACT_ID, version,
505 binaries);
506 indexes.add(binariesArtifact);
507 if (sources != null) {
508 Artifact sourcesArtifact = writeIndex(session,
509 artifactBasePath, groupId,
510 RepoConstants.SOURCES_ARTIFACT_ID, version, sources);
511 indexes.add(sourcesArtifact);
512 }
513 // sdk
514 writeIndex(session, artifactBasePath, groupId,
515 RepoConstants.SDK_ARTIFACT_ID, version, indexes);
516 session.save();
517 } catch (RepositoryException e) {
518 throw new SlcException("Cannot write indexes for group " + groupId,
519 e);
520 }
521 }
522
523 /** Write a group index. */
524 private static Artifact writeIndex(Session session,
525 String artifactBasePath, String groupId, String artifactId,
526 String version, Set<Artifact> artifacts) throws RepositoryException {
527 Artifact artifact = new DefaultArtifact(groupId, artifactId, "pom",
528 version);
529 String pom = MavenConventionsUtils.artifactsAsDependencyPom(artifact,
530 artifacts, null);
531 Node node = RepoUtils.copyBytesAsArtifact(
532 session.getNode(artifactBasePath), artifact, pom.getBytes());
533 addMavenChecksums(node);
534 return artifact;
535 }
536
537 /** Add files containing the SHA-1 and MD5 checksums. */
538 public static void addMavenChecksums(Node node) throws RepositoryException {
539 // TODO optimize
540 String sha = JcrUtils.checksumFile(node, "SHA-1");
541 JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".sha1",
542 sha.getBytes());
543 String md5 = JcrUtils.checksumFile(node, "MD5");
544 JcrUtils.copyBytesAsFile(node.getParent(), node.getName() + ".md5",
545 md5.getBytes());
546 }
547
548 /**
549 * Custom copy since the one in commons does not fit the needs when copying
550 * a workspace completely.
551 */
552 public static void copy(Node fromNode, Node toNode) {
553 copy(fromNode, toNode, null);
554 }
555
556 public static void copy(Node fromNode, Node toNode, ArgeoMonitor monitor) {
557 try {
558 String fromPath = fromNode.getPath();
559 if (monitor != null)
560 monitor.subTask("copying node :" + fromPath);
561 if (log.isDebugEnabled())
562 log.debug("copy node :" + fromPath);
563
564 // FIXME : small hack to enable specific workspace copy
565 if (fromNode.isNodeType("rep:ACL")
566 || fromNode.isNodeType("rep:system")) {
567 if (log.isTraceEnabled())
568 log.trace("node " + fromNode + " skipped");
569 return;
570 }
571
572 // add mixins
573 for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
574 toNode.addMixin(mixinType.getName());
575 }
576
577 // Double check
578 for (NodeType mixinType : toNode.getMixinNodeTypes()) {
579 if (log.isDebugEnabled())
580 log.debug(mixinType.getName());
581 }
582
583 // process properties
584 PropertyIterator pit = fromNode.getProperties();
585 properties: while (pit.hasNext()) {
586 Property fromProperty = pit.nextProperty();
587 String propName = fromProperty.getName();
588 try {
589 String propertyName = fromProperty.getName();
590 if (toNode.hasProperty(propertyName)
591 && toNode.getProperty(propertyName).getDefinition()
592 .isProtected())
593 continue properties;
594
595 if (fromProperty.getDefinition().isProtected())
596 continue properties;
597
598 if (propertyName.equals("jcr:created")
599 || propertyName.equals("jcr:createdBy")
600 || propertyName.equals("jcr:lastModified")
601 || propertyName.equals("jcr:lastModifiedBy"))
602 continue properties;
603
604 if (fromProperty.isMultiple()) {
605 toNode.setProperty(propertyName,
606 fromProperty.getValues());
607 } else {
608 toNode.setProperty(propertyName,
609 fromProperty.getValue());
610 }
611 } catch (RepositoryException e) {
612 throw new SlcException("Cannot property " + propName, e);
613 }
614 }
615
616 // recursively process children nodes
617 NodeIterator nit = fromNode.getNodes();
618 while (nit.hasNext()) {
619 Node fromChild = nit.nextNode();
620 Integer index = fromChild.getIndex();
621 String nodeRelPath = fromChild.getName() + "[" + index + "]";
622 Node toChild;
623 if (toNode.hasNode(nodeRelPath))
624 toChild = toNode.getNode(nodeRelPath);
625 else
626 toChild = toNode.addNode(fromChild.getName(), fromChild
627 .getPrimaryNodeType().getName());
628 copy(fromChild, toChild);
629 }
630
631 // update jcr:lastModified and jcr:lastModifiedBy in toNode in
632 // case
633 // they existed
634 if (!toNode.getDefinition().isProtected()
635 && toNode.isNodeType(NodeType.MIX_LAST_MODIFIED))
636 JcrUtils.updateLastModified(toNode);
637
638 // Workaround to reduce session size: artifact is a saveable
639 // unity
640 if (toNode.isNodeType(SlcTypes.SLC_ARTIFACT))
641 toNode.getSession().save();
642
643 if (monitor != null)
644 monitor.worked(1);
645
646 } catch (RepositoryException e) {
647 throw new SlcException("Cannot copy " + fromNode + " to " + toNode,
648 e);
649 }
650 }
651
652 }