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