diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginId.java b/scm-core/src/main/java/sonia/scm/plugin/PluginId.java new file mode 100644 index 0000000000..bcebfb0f08 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginId.java @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Joiner; +import com.google.common.base.Objects; + +/** + * Id of a plugin. The id of a plugin consists of the groupId, artifactId and + * the version of the plugin. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class PluginId +{ + + /** Field description */ + private static final String DELIMITER = ":"; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param groupId + * @param artifactId + * @param version + */ + public PluginId(String groupId, String artifactId, String version) + { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param obj + * + * @return + */ + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final PluginId other = (PluginId) obj; + + return Objects.equal(this.groupId, other.groupId) + && Objects.equal(this.artifactId, other.artifactId) + && Objects.equal(this.version, other.version); + } + + /** + * Method description + * + * + * @return + */ + @Override + public int hashCode() + { + return Objects.hashCode(groupId, artifactId, version); + } + + /** + * Method description + * + * + * @return + */ + @Override + public String toString() + { + return Joiner.on(DELIMITER).join(groupId, artifactId, version); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + */ + public String getArtifactId() + { + return artifactId; + } + + /** + * Method description + * + * + * @return + */ + public String getGroupId() + { + return groupId; + } + + /** + * Method description + * + * + * @return + */ + public String getId() + { + return toString(); + } + + /** + * Method description + * + * + * @return + */ + public String getIdWithoutVersion() + { + return Joiner.on(DELIMITER).join(groupId, artifactId); + } + + /** + * Method description + * + * + * @return + */ + public String getVersion() + { + return version; + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final String artifactId; + + /** Field description */ + private final String groupId; + + /** Field description */ + private final String version; +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java new file mode 100644 index 0000000000..3cfb120bcf --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/SmpArchive.java @@ -0,0 +1,400 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import com.google.common.io.Resources; + +import org.w3c.dom.Document; + +import org.xml.sax.SAXException; + +import sonia.scm.util.IOUtil; +import sonia.scm.util.XmlUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; +import java.nio.charset.Charset; + +import java.util.Collection; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.ParserConfigurationException; + +/** + * Smp plugin archive. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class SmpArchive +{ + + /** Field description */ + public static final String PATH_DESCRIPTOR = "/WEB-INF/classes/META-INF/scm/plugin.xml"; + + /** Field description */ + private static final String EL_ARTIFACTID = "artifactId"; + + /** Field description */ + private static final String EL_GROUPID = "groupId"; + + /** Field description */ + private static final String EL_VERSION = "version"; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param archive + */ + public SmpArchive(ByteSource archive) + { + this.archive = archive; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param archive + * + * @return + */ + public static SmpArchive create(ByteSource archive) + { + return new SmpArchive(archive); + } + + /** + * Method description + * + * + * @param archive + * + * @return + */ + public static SmpArchive create(URL archive) + { + return create(Resources.asByteSource(archive)); + } + + /** + * Method description + * + * + * @param archive + * + * @return + */ + public static SmpArchive create(File archive) + { + return create(Files.asByteSource(archive)); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param map + * @param key + * @param + * @param + * + * @return + */ + private static V getSingleValue(Multimap map, K key) + { + V value = null; + Collection values = map.get(key); + + if (!values.isEmpty()) + { + value = values.iterator().next(); + } + + return value; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param target + * + * @throws IOException + */ + public void extract(File target) throws IOException + { + try (ZipInputStream zis = open()) + { + ZipEntry ze = zis.getNextEntry(); + + while (ze != null) + { + + String fileName = ze.getName(); + File file = new File(target, fileName); + + IOUtil.mkdirs(file.getParentFile()); + + try (FileOutputStream fos = new FileOutputStream(file)) + { + ByteStreams.copy(zis, fos); + } + + ze = zis.getNextEntry(); + } + + zis.closeEntry(); + } + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + public Document getDescriptorDocument() throws IOException + { + if (descriptorDocument == null) + { + try + { + descriptorDocument = createDescriptorDocument(); + } + catch (ParserConfigurationException | SAXException ex) + { + throw new PluginException("could not parse descriptor", ex); + } + } + + return descriptorDocument; + } + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + public PluginId getPluginId() throws IOException + { + if (pluginId == null) + { + pluginId = createPluginId(); + } + + return pluginId; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * + * @return + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + */ + private Document createDescriptorDocument() + throws IOException, ParserConfigurationException, SAXException + { + Document doc = null; + + NonClosingZipInputStream zis = null; + try + { + zis = openNonClosing(); + ZipEntry entry = zis.getNextEntry(); + + while (entry != null) + { + if (PATH_DESCRIPTOR.equals(getPath(entry))) + { + doc = XmlUtil.createDocument(zis); + } + + entry = zis.getNextEntry(); + } + + zis.closeEntry(); + } + finally + { + if (zis != null){ + zis.reallyClose(); + } + } + + if (doc == null) + { + throw new PluginException("could not find descritor"); + } + + return doc; + } + + private static String getPath(ZipEntry entry) + { + String path = entry.getName().replace("\\", "/"); + if ( ! path.startsWith("/") ){ + path = "/".concat(path); + } + return path; + } + + private static class NonClosingZipInputStream extends ZipInputStream { + + public NonClosingZipInputStream(InputStream in, Charset charset) + { + super(in, charset); + } + + @Override + public void close() throws IOException + { + // do nothing + } + + public void reallyClose() throws IOException{ + super.close(); + } + + } + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + private PluginId createPluginId() throws IOException + { + Multimap entries = XmlUtil.values(getDescriptorDocument(), + EL_GROUPID, EL_ARTIFACTID, EL_VERSION); + String groupId = getSingleValue(entries, EL_GROUPID); + + if (Strings.isNullOrEmpty(groupId)) + { + throw new PluginException("could not find groupId in plugin descriptor"); + } + + String artifactId = getSingleValue(entries, EL_ARTIFACTID); + + if (Strings.isNullOrEmpty(artifactId)) + { + throw new PluginException( + "could not find artifactId in plugin descriptor "); + } + + String version = getSingleValue(entries, EL_VERSION); + + if (Strings.isNullOrEmpty(version)) + { + throw new PluginException("could not find version in plugin descriptor "); + } + + return new PluginId(groupId, artifactId, version); + } + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + private ZipInputStream open() throws IOException + { + return new ZipInputStream(archive.openStream(), Charsets.UTF_8); + } + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + private NonClosingZipInputStream openNonClosing() throws IOException + { + return new NonClosingZipInputStream(archive.openStream(), Charsets.UTF_8); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final ByteSource archive; + + /** Field description */ + private Document descriptorDocument; + + /** Field description */ + private PluginId pluginId; +} diff --git a/scm-core/src/main/java/sonia/scm/util/XmlUtil.java b/scm-core/src/main/java/sonia/scm/util/XmlUtil.java index 435dc85072..5249382613 100644 --- a/scm-core/src/main/java/sonia/scm/util/XmlUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/XmlUtil.java @@ -52,6 +52,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; /** + * Util methods to handle xml files. * * @author Sebastian Sdorra * @since 2.0.0 @@ -67,6 +68,26 @@ public final class XmlUtil //~--- methods -------------------------------------------------------------- + /** + * Create {@link Document} from {@link InputStream}. + * + * + * @param stream input stream + * + * @return generated document + * + * + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + */ + public static Document createDocument(InputStream stream) + throws ParserConfigurationException, SAXException, IOException + { + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( + stream); + } + /** * Method description * @@ -90,22 +111,7 @@ public final class XmlUtil { Document doc = createDocument(input); - for (String entry : entries) - { - NodeList list = doc.getElementsByTagName(entry); - - for (int i = 0; i < list.getLength(); i++) - { - Node node = list.item(i); - String value = node.getTextContent(); - - if (value != null) - { - values.put(entry, value); - } - } - } - + values(values, doc, entries); } catch (DOMException | ParserConfigurationException | SAXException ex) { @@ -120,19 +126,54 @@ public final class XmlUtil * Method description * * - * @param stream + * @param doc + * @param entries * * @return * + * @throws IOException + */ + public static Multimap values(Document doc, String... entries) + throws IOException + { + Multimap values = HashMultimap.create(); + + if ((entries != null) && (entries.length > 0)) + { + values(values, doc, entries); + } + + return values; + } + + /** + * Method description + * + * + * @param values + * @param doc + * @param entries * * @throws IOException - * @throws ParserConfigurationException - * @throws SAXException */ - private static Document createDocument(InputStream stream) - throws ParserConfigurationException, SAXException, IOException + private static void values(Multimap values, Document doc, + String... entries) + throws IOException { - return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( - stream); + for (String entry : entries) + { + NodeList list = doc.getElementsByTagName(entry); + + for (int i = 0; i < list.getLength(); i++) + { + Node node = list.item(i); + String value = node.getTextContent(); + + if (value != null) + { + values.put(entry, value); + } + } + } } } diff --git a/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java new file mode 100644 index 0000000000..1dcd35f5c3 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/plugin/SmpArchiveTest.java @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.io.Files; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.w3c.dom.Document; + +import org.xml.sax.SAXException; + +import sonia.scm.util.IOUtil; +import sonia.scm.util.XmlUtil; + +import static org.junit.Assert.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * + * @author Sebastian Sdorra + */ +public class SmpArchiveTest +{ + + /** + * Method description + * + * + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException + */ + @Test + public void testExtract() + throws IOException, ParserConfigurationException, SAXException + { + File archive = createArchive("sonia.sample", "sample", "1.0"); + File target = tempFolder.newFolder(); + + IOUtil.mkdirs(target); + SmpArchive.create(archive).extract(target); + + File descriptor = new File(target, SmpArchive.PATH_DESCRIPTOR.substring(1)); + + assertTrue(descriptor.exists()); + + try (FileInputStream fis = new FileInputStream(descriptor)) + { + Document doc = XmlUtil.createDocument(fis); + + assertEquals("plugin", doc.getDocumentElement().getNodeName()); + } + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testGetDescriptorDocument() throws IOException + { + File archive = createArchive("sonia.sample", "sample", "1.0"); + Document doc = SmpArchive.create(archive).getDescriptorDocument(); + + assertNotNull(doc); + assertEquals("plugin", doc.getDocumentElement().getNodeName()); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testGetPluginId() throws IOException + { + File archive = createArchive("sonia.sample", "sample", "1.0"); + PluginId pluginId = SmpArchive.create(archive).getPluginId(); + + assertNotNull(pluginId); + assertEquals("sonia.sample", pluginId.getGroupId()); + assertEquals("sample", pluginId.getArtifactId()); + assertEquals("1.0", pluginId.getVersion()); + } + + /** + * Method description + * + * @throws IOException + */ + @Test(expected = PluginException.class) + public void testWithMissingArtifactId() throws IOException + { + File archive = createArchive("sonia.sample", null, "1.0"); + + SmpArchive.create(archive).getPluginId(); + } + + /** + * Method description + * + * @throws IOException + */ + @Test(expected = PluginException.class) + public void testWithMissingGroupId() throws IOException + { + File archive = createArchive(null, "sample", "1.0"); + + SmpArchive.create(archive).getPluginId(); + } + + /** + * Method description + * + * @throws IOException + */ + @Test(expected = PluginException.class) + public void testWithMissingVersion() throws IOException + { + File archive = createArchive("sonia.sample", "sample", null); + + SmpArchive.create(archive).getPluginId(); + } + + /** + * Method description + * + * + * @param groupId + * @param artifactId + * @param version + * + * @return + */ + private File createArchive(String groupId, String artifactId, String version) + { + File archiveFile; + + try + { + File descriptor = tempFolder.newFile(); + + writeDescriptor(descriptor, groupId, artifactId, version); + archiveFile = tempFolder.newFile(); + + try (ZipOutputStream zos = + new ZipOutputStream(new FileOutputStream(archiveFile), Charsets.UTF_8)) + { + zos.putNextEntry(new ZipEntry(SmpArchive.PATH_DESCRIPTOR)); + Files.copy(descriptor, zos); + zos.closeEntry(); + } + } + catch (IOException ex) + { + throw Throwables.propagate(ex); + } + + return archiveFile; + } + + /** + * Method description + * + * + * @param file + * + * @return + * + * @throws IOException + * @throws XMLStreamException + */ + private XMLStreamWriter createStreamWriter(File file) + throws IOException, XMLStreamException + { + return XMLOutputFactory.newFactory().createXMLStreamWriter( + new FileOutputStream(file)); + } + + /** + * Method description + * + * + * @param descriptor + * @param groupId + * @param artifactId + * @param version + * + * @throws IOException + */ + private void writeDescriptor(File descriptor, String groupId, + String artifactId, String version) + throws IOException + { + try + { + + IOUtil.mkdirs(descriptor.getParentFile()); + + XMLStreamWriter writer = null; + + try + { + writer = createStreamWriter(descriptor); + writer.writeStartDocument(); + writer.writeStartElement("plugin"); + writer.writeStartElement("information"); + writeElement(writer, "groupId", groupId); + writeElement(writer, "artifactId", artifactId); + writeElement(writer, "version", version); + + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + finally + { + if (writer != null) + { + writer.close(); + } + } + } + catch (XMLStreamException ex) + { + throw Throwables.propagate(ex); + } + } + + /** + * Method description + * + * + * @param writer + * @param name + * @param value + * + * @throws XMLStreamException + */ + private void writeElement(XMLStreamWriter writer, String name, String value) + throws XMLStreamException + { + if (!Strings.isNullOrEmpty(value)) + { + writer.writeStartElement(name); + writer.writeCharacters(value); + writer.writeEndElement(); + } + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); +}