de.dentrassi.pm.deb.aspect.internal.RepoBuilder.java Source code

Java tutorial

Introduction

Here is the source code for de.dentrassi.pm.deb.aspect.internal.RepoBuilder.java

Source

/*******************************************************************************
 * 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 de.dentrassi.pm.deb.aspect.internal;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.eclipse.scada.utils.str.StringHelper;

import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;

import de.dentrassi.pm.common.Severity;
import de.dentrassi.pm.common.utils.HashHelper;
import de.dentrassi.pm.deb.aspect.DistributionInformation;
import de.dentrassi.pm.signing.SigningService;
import de.dentrassi.pm.utils.deb.Packages;

/**
 * Build a repository <br/>
 * Before packages are added with
 * {@link #addPackage(String, String, String, PackageInformation)} all
 * distributions must be created with
 * {@link #addDistribution(String, DistributionInformation)}
 */
public class RepoBuilder {

    private static DateFormat DATE_FORMAT;

    static {
        DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM YYYY HH:mm:ss z", Locale.US);
        DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    }

    public static class Distribution {
        private final String name;

        private final DistributionInformation information;

        private final Map<String, Component> components = new TreeMap<>();

        public Distribution(final String name, final DistributionInformation information) {
            this.name = name;
            this.information = information;

            // populate components
            for (final String component : information.getComponents()) {
                this.components.put(component, new Component(component, information.getArchitectures()));
            }
        }

        public DistributionInformation getInformation() {
            return this.information;
        }

        public Map<String, Component> getComponents() {
            return Collections.unmodifiableMap(this.components);
        }

        public String getName() {
            return this.name;
        }

        public void addPackage(final String component, final String architecture,
                final PackageInformation packageInfo, final ValidationListener validationListener) {
            final Component comp = this.components.get(component);
            if (comp == null) {
                return;
            }

            if ("all".equals(architecture)) {
                for (final String arch : this.information.getArchitectures()) {
                    comp.addPackage("binary-" + arch, arch, packageInfo);
                }
            } else {
                if (!this.information.getArchitectures().contains(architecture)) {
                    if (validationListener != null) {
                        validationListener.validationMessage(Severity.WARNING, String.format(
                                "Architecture '%s' is not configured. Package will be ignored.", architecture));
                    }
                    return;
                }

                comp.addPackage("binary-" + architecture, architecture, packageInfo);
            }
        }
    }

    public static class Component {
        private final String name;

        private final Map<String, SubComponent> subComponents = new HashMap<>();

        public Component(final String name, final SortedSet<String> archs) {
            this.name = name;

            for (final String arch : archs) {
                final String subName = "binary-" + arch;
                this.subComponents.put(subName, new SubComponent(subName, arch));
            }
        }

        public String getName() {
            return this.name;
        }

        public void addPackage(final String subComponent, final String architecture,
                final PackageInformation packageInfo) {
            final SubComponent sub = this.subComponents.get(subComponent);
            if (sub == null) {
                return;
            }

            sub.addPackage(packageInfo);
        }

        public Map<String, SubComponent> getSubComponents() {
            return Collections.unmodifiableMap(this.subComponents);
        }
    }

    public static class SubComponent {
        private final String name;

        private final StringWriter sw = new StringWriter();

        private final PrintWriter pw = new PrintWriter(this.sw);

        private final String architecture;

        public SubComponent(final String name, final String architecture) {
            this.name = name;
            this.architecture = architecture;
        }

        public String getName() {
            return this.name;
        }

        public String getArchitecture() {
            return this.architecture;
        }

        public void addPackage(final PackageInformation packageInfo) {
            final Map<String, String> values = new HashMap<>(packageInfo.getControl());

            values.put("Filename", packageInfo.getPoolName());
            values.put("Size", "" + packageInfo.getFileSize());

            if (!values.containsKey("Description-md5")) {
                values.put("Description-md5", Packages.makeDescriptionMd5(values.get("Description")));
            }

            // add checksum entries

            for (final Map.Entry<String, String> entry : packageInfo.getChecksums().entrySet()) {
                final String v = entry.getValue();
                if (v != null) {
                    values.put(entry.getKey(), v);
                }
            }

            try {
                Packages.writeBinaryPackageValues(this.pw, values);
            } catch (final IOException e) {
                throw new RuntimeException("Failed to write package stream", e);
            }

            this.pw.print("\n");
        }

        public byte[] toReleaseFile(final Distribution dist, final Component comp) {
            final StringWriter sw = new StringWriter();

            final DistributionInformation info = dist.getInformation();

            writeOptional(sw, "Version", info.getVersion());
            writeOptional(sw, "Origin", info.getOrigin());
            writeOptional(sw, "Label", info.getLabel());
            writeOptional(sw, "Archive", info.getSuite());

            sw.write("Component: " + comp.getName() + "\n");
            sw.write("Architecture: " + this.architecture + "\n");

            return sw.toString().getBytes(StandardCharsets.UTF_8);
        }

        public byte[] toPackageFile() {
            return this.sw.toString().getBytes(StandardCharsets.UTF_8);
        }
    }

    public static class PackageInformation {
        private final String poolName;

        private final Map<String, String> control;

        private final long fileSize;

        private final Map<String, String> checksums;

        public PackageInformation(final String poolName, final long fileSize, final Map<String, String> control,
                final Map<String, String> checksums) {
            this.poolName = poolName;
            this.fileSize = fileSize;
            this.control = control;
            this.checksums = checksums;
        }

        public long getFileSize() {
            return this.fileSize;
        }

        public Map<String, String> getControl() {
            return this.control;
        }

        public String getPoolName() {
            return this.poolName;
        }

        public Map<String, String> getChecksums() {
            return this.checksums;
        }
    }

    private final Map<String, Distribution> distributions = new HashMap<>();

    private final SigningService signingService;

    public RepoBuilder(final SigningService signingService) {
        this.signingService = signingService;
    }

    public void addDistribution(final String name, final DistributionInformation information) {
        this.distributions.put(name, new Distribution(name, information));
    }

    public void addPackage(final String distribution, final String component, final String architecture,
            final PackageInformation packageInfo) {
        addPackage(distribution, component, architecture, packageInfo, null);
    }

    public void addPackage(final String distribution, final String component, final String architecture,
            final PackageInformation packageInfo, final ValidationListener validationListener) {
        final Distribution dist = this.distributions.get(distribution);
        if (dist == null) {
            return; // ignore
        }

        dist.addPackage(component, architecture, packageInfo, validationListener);
    }

    private static class Checksums {
        private final Map<String, HashCode> codes;

        private final int size;

        public Checksums(final Map<String, HashCode> result, final int length) {
            this.codes = result;
            this.size = length;
        }

        public Map<String, HashCode> getCodes() {
            return this.codes;
        }

        public int getSize() {
            return this.size;
        }

        public static Checksums create(final byte[] data) throws IOException {
            final Map<String, HashFunction> functions = new HashMap<>();

            functions.put("MD5Sum", Hashing.md5());
            functions.put("SHA1", Hashing.sha1());
            functions.put("SHA256", Hashing.sha256());

            final Map<String, HashCode> result = HashHelper.createChecksums(new ByteArrayInputStream(data),
                    functions);

            return new Checksums(result, data.length);
        }
    }

    public void spoolOut(final SpoolOutHandler handler) throws IOException {
        for (final Distribution dist : this.distributions.values()) {
            final SortedMap<String, Checksums> checksums = new TreeMap<>();

            for (final Component comp : dist.getComponents().values()) {
                for (final SubComponent sub : comp.getSubComponents().values()) {
                    spoolOutFile(checksums, "dists/" + dist.getName(),
                            String.format("%s/%s/Release", comp.getName(), sub.getName()), "text/plain",
                            sub.toReleaseFile(dist, comp), handler);

                    final byte[] pkgData = sub.toPackageFile();

                    spoolOutFile(checksums, "dists/" + dist.getName(),
                            String.format("%s/%s/Packages", comp.getName(), sub.getName()), "text/plain", pkgData,
                            handler);
                    spoolOutFile(checksums, "dists/" + dist.getName(),
                            String.format("%s/%s/Packages.gz", comp.getName(), sub.getName()), "application/x-gzip",
                            compressGzip(pkgData), handler);
                    spoolOutFile(checksums, "dists/" + dist.getName(),
                            String.format("%s/%s/Packages.bz2", comp.getName(), sub.getName()),
                            "application/x-bzip2", compressBzip2(pkgData), handler);
                }
            }

            spoolOutDistRelease(dist, checksums, handler);
        }
    }

    private byte[] compressGzip(final byte[] data) throws IOException {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final GZIPOutputStream gos = new GZIPOutputStream(bos);

        gos.write(data);

        gos.close();
        return bos.toByteArray();
    }

    private byte[] compressBzip2(final byte[] data) throws IOException {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final BZip2CompressorOutputStream b2os = new BZip2CompressorOutputStream(bos);

        b2os.write(data);

        b2os.close();
        return bos.toByteArray();
    }

    private void spoolOutDistRelease(final Distribution dist, final SortedMap<String, Checksums> checksums,
            final SpoolOutHandler handler) throws IOException {
        final StringWriter sw = new StringWriter();

        final DistributionInformation info = dist.getInformation();

        writeOptional(sw, "Origin", info.getOrigin());
        writeOptional(sw, "Label", info.getLabel());
        writeOptional(sw, "Suite", info.getSuite());
        writeOptional(sw, "Version", info.getVersion());
        writeOptional(sw, "Codename", info.getCodename());
        write(sw, "Date", DATE_FORMAT.format(new Date()));
        write(sw, "Components", StringHelper.join(dist.getComponents().keySet(), " "));
        write(sw, "Architectures", StringHelper.join(info.getArchitectures(), " "));
        writeOptional(sw, "Description", info.getDescription());

        {
            // create checksum fields

            final StringWriter md5 = new StringWriter();
            final StringWriter sha1 = new StringWriter();
            final StringWriter sha256 = new StringWriter();
            final PrintWriter md5Pw = new PrintWriter(md5);
            final PrintWriter sha1Pw = new PrintWriter(sha1);
            final PrintWriter sha256Pw = new PrintWriter(sha256);

            for (final Map.Entry<String, Checksums> entry : checksums.entrySet()) {
                addChecksum(md5Pw, entry.getKey(), entry.getValue(), "MD5Sum");
                addChecksum(sha1Pw, entry.getKey(), entry.getValue(), "SHA1");
                addChecksum(sha256Pw, entry.getKey(), entry.getValue(), "SHA256");
            }

            write(sw, "MD5Sum", md5.toString());
            write(sw, "SHA1", sha1.toString());
            write(sw, "SHA256", sha256.toString());
        }

        final byte[] data = sw.toString().getBytes(StandardCharsets.UTF_8);
        handler.spoolOut(String.format("dists/%s/Release", dist.getName()), "text/plain",
                new ByteArrayInputStream(data));

        if (this.signingService != null) {
            handler.spoolOut(String.format("dists/%s/Release.gpg", dist.getName()), "text/plain",
                    new ByteArrayInputStream(sign(data, false)));
            handler.spoolOut(String.format("dists/%s/InRelease", dist.getName()), "text/plain",
                    new ByteArrayInputStream(sign(data, true)));

            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
            this.signingService.printPublicKey(bos);
            bos.close();

            handler.spoolOut("GPG-KEY", "text/plain", new ByteArrayInputStream(bos.toByteArray()));
        }
    }

    private byte[] sign(final byte[] data, final boolean inline) throws IOException {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            this.signingService.sign(new ByteArrayInputStream(data), bos, inline);
        } catch (final Exception e) {
            throw new RuntimeException("Failed to sign", e);
        } finally {
            bos.close();
        }

        return bos.toByteArray();
    }

    private void addChecksum(final PrintWriter writer, final String name, final Checksums chk, final String alg) {
        final HashCode hashCode = chk.getCodes().get(alg);
        writer.format("\n %s %16d %s", hashCode.toString(), chk.getSize(), name);
    }

    private void spoolOutFile(final Map<String, Checksums> checksums, final String prefix, final String fileName,
            final String mimeType, final byte[] data, final SpoolOutHandler handler) throws IOException {
        checksums.put(fileName, Checksums.create(data));
        handler.spoolOut(prefix + "/" + fileName, mimeType, new ByteArrayInputStream(data));
    }

    /**
     * Write field only when the value is set
     *
     * @param writer
     *            the writer to use
     * @param fieldName
     *            the name of field
     * @param value
     *            the value
     */
    protected static void writeOptional(final StringWriter writer, final String fieldName, final String value) {
        if (value != null) {
            write(writer, fieldName, value);
        }
    }

    /**
     * Write a field
     *
     * @param writer
     *            the writer to use
     * @param fieldName
     *            the field name
     * @param value
     *            the value, should not be <code>null</code> since this would
     *            cause the string
     *            <q>null</q> in the file.
     */
    protected static void write(final StringWriter writer, final String fieldName, final String value) {
        writer.write(fieldName + ": " + value + "\n");
    }
}