 /*
  * RHQ Management Platform
  * Copyright (C) 2005-2008 Red Hat, Inc.
  * All rights reserved.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License, version 2, as
  * published by the Free Software Foundation, and/or the GNU Lesser
  * General Public License, version 2.1, also as published by the Free
  * Software Foundation.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU General Public License and the GNU Lesser General Public License
  * for more details.
  *
  * You should have received a copy of the GNU General Public License
  * and the GNU Lesser General Public License along with this program;
  * if not, write to the Free Software Foundation, Inc.,
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
package org.rhq.plugins.platform.content.yum;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.composite.PackageVersionMetadataComposite;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;

/**
 * Represents the primary <i>yum</i> metadata file (primary.xml).
 *
 * @author jortel
 */
public class Primary extends Content {
    /**
     * file mutex.
     */
    private static ReentrantLock lock = new ReentrantLock();

    /**
     * Locally stored primary.xml MD5.
     */
    private final PrimaryMD5 mymd5;

    /**
     * Construct the object with an active request.
     *
     * @param request An active yum request.
     */
    public Primary(Request request) {
        super(request);
        mymd5 = new PrimaryMD5(context().getTemporaryDirectory());
    }

    /*
     * (non-Javadoc) @see org.jboss.on.plugins.platform.content.yum.Content#openStream()
     */
    @Override
    public InputStream openStream() throws Exception {
        return new FileInputStream(file());
    }

    /*
     * (non-Javadoc) @see org.jboss.on.plugins.platform.content.yum.Content#writeHeader(java.io.OutputStream)
     */
    @Override
    public void writeHeader(OutputStream ostr) throws Exception {
        PrintWriter writer = new PrintWriter(ostr);
        writer.printf("HTTP/1.1 200\n");
        writer.println("Server: Ackbar (Red Hat)");
        writer.println("Content-Type: text/xml; charset=utf-8");
        writer.printf("Content-Length: %d\n\n", length());
        writer.flush();
    }

    /*
     * (non-Javadoc) @see org.jboss.on.plugins.platform.content.yum.Content#writeContent(java.io.OutputStream)
     */
    @Override
    public void writeContent(OutputStream ostr) throws Exception {
        InputStream istr = openStream();
        transfer(istr, ostr);
        istr.close();
    }

    /*
     * (non-Javadoc) @see org.jboss.on.plugins.platform.content.yum.Content#delete()
     */
    @Override
    public void delete() {
        mymd5.delete();
        lock.lock();
        try {
            new File(filepath()).delete();
        } finally {
            lock.unlock();
        }
    }

    /*
     * (non-Javadoc) @see org.jboss.on.plugins.platform.content.yum.Content#length()
     */
    @Override
    public long length() throws Exception {
        lock.lock();
        try {
            return file().length();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get the primary.xml file object. The fiie is created if it does not exist.
     *
     * @return The file object.
     *
     * @throws Exception
     */
    File file() throws Exception {
        lock.lock();
        try {
            File file = new File(filepath());
            if (!file.exists()) {
                create(file);
            }

            return file;
        } catch (Exception e) {
            delete();
            throw e;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get whether the primary metadata is stale.
     *
     * <p/>The data is stale when the md5 stored locally does not match the server's md5.
     *
     * <p/>The md5 is the checksum of all of the packages in all of the repos to which a resource is subscribed. The
     * locally stored value is compared with the server's in order to detect that package metadata has changed in one or
     * more repo which means the locally cached primary.xml no longer represents the content of the subscribed
     * repos.
     *
     * @return
     */
    boolean stale() {
        try {
            String md5 = context().getResourceSubscriptionMD5();
            if (!mymd5.matches(md5)) {
                return true;
            }
        } catch (IOException e) {
            log.error("Error validating/writing md5", e);
            return true;
        }

        return false;
    }

    /**
     * Get the object's local file path. The primary.xml is constructed locally on the filesystem.
     *
     * @return The object's local file path.
     */
    private String filepath() {
        return new File(context().getTemporaryDirectory(), "primary.xml").getAbsolutePath();
    }

    /**
     * Create the <i>local</i> primary.xml file. This consists of querying the server for all package version metadata
     * that this resource is associated with. Each package metadata blob is expected to be a yum package entry from a
     * primary.xml file. The location is replaced with an encoded list of http arguments (?,,) as needed to construct
     * the PackageDetailsKey when yum calls for the package.
     *
     * @param  file The file to create.
     *
     * @throws IOException On errors.
     */
    private void create(File file) throws IOException {
        long start = System.currentTimeMillis();
        PrintWriter writer = new PrintWriter(file);
        Pattern ptn = Pattern.compile("(href=\")([^\"]+)(\")");
        PageControl pc = new PageControl();
        pc.setPageNumber(0);
        pc.setPageSize(200);

        mymd5.write(context().getResourceSubscriptionMD5());

        while (true) {
            PageList<PackageVersionMetadataComposite> list = context().getPackageVersionMetadata(pc);
            if (pc.getPageNumber() == 0) {
                writer.append("<metadata xmlns=\"http://linux.duke.edu/metadata/common\" ");
                writer.append("xmlns:rpm=\"http://linux.duke.edu/metadata/rpm\" ");
                writer.printf("packages=\"%d\">\n", list.getTotalSize());
            }

            if (list.size() < 1) {
                break;
            }

            for (PackageVersionMetadataComposite p : list) {
                byte[] metadata = p.getMetadata();
                String pkg = new String(gunzip(metadata));
                Matcher m = ptn.matcher(pkg);
                m.find();
                StringBuilder sb = new StringBuilder(pkg);
                sb.insert(m.end(2), toArgs(p.getPackageDetailsKey()));
                writer.append(sb.toString());
            }

            pc.setPageNumber(pc.getPageNumber() + 1);
        }

        writer.append("</metadata>");
        writer.flush();
        writer.close();

        long duration = (System.currentTimeMillis() - start);
        log.info("file: " + file + " created: " + duration + " (ms)");
    }

    /**
     * Unzip the input bytes into a string. Uncompression exceptions are tolorated to support databases containing
     * uncompressed data.
     *
     * @param  input An array of gzipped bytes.
     *
     * @return A string.
     */
    private String gunzip(byte[] input) {
        try {
            InputStream zipped = new GZIPInputStream(new ByteArrayInputStream(input), bfr.length);
            ByteArrayOutputStream unzipped = new ByteArrayOutputStream();
            while (true) {
                int bytesRead = zipped.read(bfr);
                if (bytesRead != -1) {
                    unzipped.write(bfr, 0, bytesRead);
                } else {
                    break;
                }
            }

            return unzipped.toString();
        } catch (Exception e) {
            log.debug("compressed data expected, gunzip failed", e);
        }

        return new String(input);
    }

    /**
     * Convert the package key into an http arg string: <i>?type=,name=,ver=arch=</i>.
     *
     * @param  key The package key to convert.
     *
     * @return An http args representation of the key.
     */
    static String toArgs(PackageDetailsKey key) {
        StringBuilder sb = new StringBuilder("?");
        sb.append("type=");
        sb.append(key.getPackageTypeName());
        sb.append(",name=");
        sb.append(key.getName());
        sb.append(",ver=");
        sb.append(key.getVersion());
        sb.append(",arch=");
        sb.append(key.getArchitectureName());
        return sb.toString();
    }

    /**
     * Convert the http args (name, ver, type, arch) into a package details key object.
     *
     * @param  args An http arg string.
     *
     * @return A package details key.
     */
    static PackageDetailsKey toKey(Map<String, String> args) {
        return new PackageDetailsKey(args.get("name"), args.get("ver"), args.get("type"), args.get("arch"));
    }
}