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