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