/******************************************************************************* * Copyright (c) 2015 IBH SYSTEMS GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.packagedrone.repo.adapter.maven; import static org.eclipse.packagedrone.repo.XmlHelper.addElement; import static org.eclipse.packagedrone.repo.XmlHelper.addElementFirst; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.XmlHelper; import org.eclipse.packagedrone.repo.channel.ArtifactInformation; import org.eclipse.scada.utils.str.StringHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.google.gson.Gson; import com.google.gson.GsonBuilder; public class ChannelData { protected static final DateFormat DATE_FORMAT = new SimpleDateFormat ( "yyyyMMddHHmmss" ); private static final Pattern SNAPSHOT_PATTERN = Pattern.compile ( "(?<ts>[0-9]{8}-[0-9]{6})-1(?<bn>[0-9]+)" ); public static abstract class Node { public boolean isDirectory () { return false; } } public static class DirectoryNode extends Node { private final Map<String, Node> nodes = new HashMap<> (); public Map<String, Node> getNodes () { return this.nodes; } @Override public boolean isDirectory () { return true; } } public abstract static class ContentNode extends Node { public abstract String getMimeType (); public abstract byte[] getData (); } public static class DataNode extends ContentNode { private final byte[] data; private final String mimeType; public DataNode ( final byte[] data, final String mimeType ) { this.data = data; this.mimeType = mimeType; } public DataNode ( final String data, final String mimeType ) { this.data = data.getBytes ( StandardCharsets.UTF_8 ); this.mimeType = mimeType; } @Override public String getMimeType () { return this.mimeType; } @Override public byte[] getData () { return this.data; } } public static class ChecksumNode extends ContentNode { private final ContentNode node; private final String alg; public ChecksumNode ( final ContentNode node, final String alg ) { this.node = node; this.alg = alg; } @Override public String getMimeType () { return "text/plain"; } @Override public byte[] getData () { try { final MessageDigest md = MessageDigest.getInstance ( this.alg ); final byte[] result = md.digest ( this.node.getData () ); return StringHelper.toHex ( result ).getBytes ( StandardCharsets.UTF_8 ); } catch ( final Exception e ) { throw new RuntimeException ( e ); } } } public static class ArtifactNode extends Node { private final String artifactId; public ArtifactNode ( final String artifactId ) { this.artifactId = artifactId; } public String getArtifactId () { return this.artifactId; } } public static class VersionMetadataNode extends ContentNode { private final List<MavenInformation> infos = new LinkedList<> (); private final Map<MavenInformation, Date> timestamps = new HashMap<> (); private final String groupId; private final String artifactId; private final String version; public VersionMetadataNode ( final String groupId, final String artifactId, final String version ) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; } @Override public String getMimeType () { return "application/xml"; } @Override public byte[] getData () { final XmlHelper xml = new XmlHelper (); final Document doc = createMetaData ( this.groupId, this.artifactId, this.version, this.infos, this.timestamps ); try { return xml.toData ( doc ); } catch ( final Exception e ) { throw new RuntimeException ( e ); } } public void add ( final MavenInformation info, final Date date ) { this.infos.add ( info ); this.timestamps.put ( info, date ); } } public static class ArtifactMetadataNode extends ContentNode { private final List<MavenInformation> infos = new LinkedList<> (); private final String groupId; private final String artifactId; public ArtifactMetadataNode ( final String groupId, final String artifactId ) { this.groupId = groupId; this.artifactId = artifactId; } @Override public String getMimeType () { return "application/xml"; } @Override public byte[] getData () { final XmlHelper xml = new XmlHelper (); final Document doc = createMetaData ( this.groupId, this.artifactId, this.infos ); try { return xml.toData ( doc ); } catch ( final Exception e ) { throw new RuntimeException ( e ); } } public void add ( final MavenInformation info, final Date date ) { this.infos.add ( info ); } } private final DirectoryNode root = new DirectoryNode (); public void add ( final MavenInformation info, final ArtifactInformation art ) { final String[] gn = info.getGroupId ().split ( "\\." ); final DirectoryNode groupNode = getGroup ( gn ); final DirectoryNode artifactBase = addDirNode ( groupNode, info.getArtifactId () ); final ArtifactMetadataNode mdNode = addArtifactMetaDataNode ( info, artifactBase ); final DirectoryNode versionNode = addDirNode ( artifactBase, info.getVersion () ); addNode ( versionNode, info.makeName (), new ArtifactNode ( art.getId () ) ); addCheckSum ( versionNode, info.makeName (), art, "md5" ); addCheckSum ( versionNode, info.makeName (), art, "sha1" ); mdNode.add ( info, art.getCreationTimestamp () ); if ( info.isSnapshot () ) { final VersionMetadataNode versionMd = addVersionMetaDataNode ( info, versionNode ); versionMd.add ( info, art.getCreationTimestamp () ); } } protected <T extends ContentNode> T addMetaDataNode ( final MavenInformation info, final DirectoryNode base, final Class<T> clazz, final Supplier<T> supp ) { final Node n = base.getNodes ().get ( "maven-metadata.xml" ); if ( n == null ) { final T result = addNode ( base, "maven-metadata.xml", supp.get () ); addNode ( base, "maven-metadata.xml.md5", new ChecksumNode ( result, "MD5" ) ); addNode ( base, "maven-metadata.xml.sha1", new ChecksumNode ( result, "SHA1" ) ); return result; } else if ( clazz.isAssignableFrom ( n.getClass () ) ) { return clazz.cast ( n ); } else { throw new IllegalStateException ( String.format ( "Invalid hierarchy. Someone blocked meta data entry: 'maven-metadata.xml'" ) ); } } protected ArtifactMetadataNode addArtifactMetaDataNode ( final MavenInformation info, final DirectoryNode artifactBase ) { return addMetaDataNode ( info, artifactBase, ArtifactMetadataNode.class, () -> new ArtifactMetadataNode ( info.getGroupId (), info.getArtifactId () ) ); } protected VersionMetadataNode addVersionMetaDataNode ( final MavenInformation info, final DirectoryNode artifactBase ) { return addMetaDataNode ( info, artifactBase, VersionMetadataNode.class, () -> new VersionMetadataNode ( info.getGroupId (), info.getArtifactId (), info.getVersion () ) ); } protected static Document makeMetaData ( final String groupId, final String artifactId, final BiConsumer<Document, Element> cons ) { final XmlHelper xml = new XmlHelper (); final Document doc = xml.create (); final Element root = doc.createElement ( "metadata" ); doc.appendChild ( root ); addElement ( root, "groupId", groupId ); addElement ( root, "artifactId", artifactId ); final Element v = addElement ( root, "versioning" ); cons.accept ( doc, v ); XmlHelper.addElement ( v, "lastUpdated", DATE_FORMAT.format ( new Date () ) ); return doc; } public static Document createMetaData ( final String groupId, final String artifactId, final String version, final List<MavenInformation> infos, final Map<MavenInformation, Date> timestamps ) { return makeMetaData ( groupId, artifactId, ( doc, v ) -> { // insert right before "versioning" final Element ver = doc.createElement ( "version" ); ver.setTextContent ( version ); v.getParentNode ().insertBefore ( ver, v ); final Map<String, List<MavenInformation>> gi = new TreeMap<> (); final TreeSet<String> snapshots = new TreeSet<> (); // group by snapshot version for ( final MavenInformation info : infos ) { if ( info.isSnapshot () && info.getSnapshotVersion () == null ) { continue; } snapshots.add ( info.getSnapshotVersion () ); List<MavenInformation> list = gi.get ( info.getSnapshotVersion () ); if ( list == null ) { list = new LinkedList<> (); gi.put ( info.getSnapshotVersion (), list ); } list.add ( info ); } if ( !gi.isEmpty () ) { final Element svs = addElement ( v, "snapshotVersions" ); for ( final Map.Entry<String, List<MavenInformation>> entry : gi.entrySet () ) { for ( final MavenInformation info : entry.getValue () ) { final Element sv = addElement ( svs, "snapshotVersion" ); addElement ( sv, "extension", info.getExtension () ); addElement ( sv, "value", info.getSnapshotVersion () ); if ( info.getClassifier () != null && !info.getClassifier ().isEmpty () ) { addElement ( sv, "classifier", info.getClassifier () ); } final Date ts = timestamps.get ( info ); if ( ts != null ) { addElement ( sv, "updated", DATE_FORMAT.format ( ts ) ); // FIXME: replace with artifact date } } } { final String latest = snapshots.last (); final Matcher m = SNAPSHOT_PATTERN.matcher ( latest ); if ( m.matches () ) { final Element s = addElementFirst ( v, "snapshot" ); addElement ( s, "timestamp", m.group ( "ts" ) ); addElement ( s, "buildNumber", m.group ( "bn" ) ); } } } } ); } public static Document createMetaData ( final String groupId, final String artifactId, final List<MavenInformation> infos ) { return makeMetaData ( groupId, artifactId, ( doc, v ) -> { final Set<String> releases = new HashSet<> (); final Set<String> all = new HashSet<> (); for ( final MavenInformation info : infos ) { all.add ( info.getVersion () ); if ( info.isSnapshot () ) { continue; } releases.add ( info.getVersion () ); } final List<String> allSorted = sorted ( all ); if ( !all.isEmpty () ) { final Element vs = addElement ( v, "versions" ); for ( final String release : allSorted ) { addElement ( vs, "version", release ); } } if ( !releases.isEmpty () ) { final List<String> releasesSorted = sorted ( releases ); final String releaseStr = best ( releasesSorted ); if ( releaseStr != null ) { final Element release = addElementFirst ( v, "release" ); release.setTextContent ( releaseStr ); } } final String latestStr = best ( allSorted ); if ( latestStr != null ) { final Element latest = addElementFirst ( v, "latest" ); latest.setTextContent ( latestStr ); } } ); } private static List<String> sorted ( final Set<String> versions ) { final List<String> list = new ArrayList<> ( versions ); Collections.sort ( list ); return list; } private static String best ( final List<String> versions ) { if ( versions.isEmpty () ) { return null; } return versions.get ( versions.size () - 1 ); } private static void addCheckSum ( final DirectoryNode versionNode, final String name, final ArtifactInformation art, final String string ) { final String data = art.getMetaData ().get ( new MetaKey ( "hasher", string ) ); if ( data == null ) { return; } addNode ( versionNode, name + "." + string, new DataNode ( data, "text/plain" ) ); } private DirectoryNode getGroup ( final String[] gn ) { final LinkedList<String> dir = new LinkedList<> ( Arrays.asList ( gn ) ); DirectoryNode current = this.root; while ( !dir.isEmpty () ) { current = addDirNode ( current, dir.pollFirst () ); } return current; } private static <T extends Node> T addNode ( final DirectoryNode current, final String seg, final T node ) { if ( current.nodes.containsKey ( seg ) ) { throw new IllegalStateException ( String.format ( "Invalid hierarchy. %s is already used.", seg ) ); } current.nodes.put ( seg, node ); return node; } private DirectoryNode addDirNode ( final DirectoryNode current, final String seg ) { Node g = current.nodes.get ( seg ); if ( g == null ) { g = new DirectoryNode (); current.nodes.put ( seg, g ); return (DirectoryNode)g; } if ( g instanceof DirectoryNode ) { return (DirectoryNode)g; } throw new IllegalStateException ( String.format ( "Invalid group hierarchy. %s is of type %s.", seg, g.getClass () ) ); } public Node findNode ( final Deque<String> segs ) { Node current = this.root; while ( !segs.isEmpty () ) { if ( ! ( current instanceof DirectoryNode ) ) { return null; } final String n = segs.pollFirst (); final Node node = ( (DirectoryNode)current ).nodes.get ( n ); if ( node == null ) { return null; } current = node; } return current; } public String toJson () { final Gson gson = makeGson ( false ); return gson.toJson ( this ); } @Override public String toString () { final Gson gson = makeGson ( true ); return gson.toJson ( this ); } public static ChannelData fromJson ( final String json ) { final Gson gson = makeGson ( false ); return gson.fromJson ( json, ChannelData.class ); } public static ChannelData fromReader ( final Reader reader ) { final Gson gson = makeGson ( false ); return gson.fromJson ( reader, ChannelData.class ); } /** * Make an appropriate Gson parser to processing ChannelData instances * * @param pretty * if the gson output should be "pretty printed" * @return the new gson instance */ public static Gson makeGson ( final boolean pretty ) { final GsonBuilder gb = new GsonBuilder (); if ( pretty ) { gb.setPrettyPrinting (); } gb.registerTypeAdapter ( Node.class, new NodeAdapter () ); gb.registerTypeAdapter ( byte[].class, new ByteArrayAdapter () ); return gb.create (); } }