/*
 * This file is part of dependency-check-core.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (c) 2015 Jeremy Long. All Rights Reserved.
 */
package org.owasp.dependencycheck.data.update;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.zip.GZIPInputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.io.FileUtils;
import static org.owasp.dependencycheck.data.nvdcve.DatabaseProperties.LAST_CPE_UPDATE;
import org.owasp.dependencycheck.data.update.cpe.CPEHandler;
import org.owasp.dependencycheck.data.update.cpe.Cpe;
import org.owasp.dependencycheck.data.update.exception.UpdateException;
import org.owasp.dependencycheck.utils.DateUtil;
import org.owasp.dependencycheck.utils.DownloadFailedException;
import org.owasp.dependencycheck.utils.Downloader;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * The CpeUpdater is designed to download the CPE data file from NIST and import the data into the database. However, as this
 * currently adds no beneficial data, compared to what is in the CPE data contained in the CVE data files, this class is not
 * currently used. The code is being kept as a future update may utilize more data from the CPE xml files.
 *
 * @author Jeremy Long
 */
public class CpeUpdater extends BaseUpdater implements CachedWebDataSource {

    /**
     * Static logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(CpeUpdater.class);

    @Override
    public void update() throws UpdateException {
        try {
            openDataStores();
            if (updateNeeded()) {
                LOGGER.info("Updating the Common Platform Enumeration (CPE)");
                final File xml = downloadCpe();
                final List<Cpe> cpes = processXML(xml);
                getCveDB().deleteUnusedCpe();
                for (Cpe cpe : cpes) {
                    getCveDB().addCpe(cpe.getValue(), cpe.getVendor(), cpe.getProduct());
                }
                final Date now = new Date();
                getProperties().save(LAST_CPE_UPDATE, Long.toString(now.getTime()));
                LOGGER.info("CPE update complete");
            }
        } finally {
            closeDataStores();
        }
    }

    /**
     * Downloads the CPE XML file.
     *
     * @return the file reference to the CPE.xml file
     * @throws UpdateException thrown if there is an issue downloading the XML file
     */
    private File downloadCpe() throws UpdateException {
        File xml;
        final URL url;
        try {
            url = new URL(Settings.getString(Settings.KEYS.CPE_URL));
            xml = File.createTempFile("cpe", ".xml", Settings.getTempDirectory());
            Downloader.fetchFile(url, xml);
            if (url.toExternalForm().endsWith(".xml.gz")) {
                extractGzip(xml);
            }

        } catch (MalformedURLException ex) {
            throw new UpdateException("Invalid CPE URL", ex);
        } catch (DownloadFailedException ex) {
            throw new UpdateException("Unable to download CPE XML file", ex);
        } catch (IOException ex) {
            throw new UpdateException("Unable to create temporary file to download CPE", ex);
        }
        return xml;
    }

    /**
     * Parses the CPE XML file to return a list of CPE entries.
     *
     * @param xml the CPE data file
     * @return the list of CPE entries
     * @throws UpdateException thrown if there is an issue with parsing the XML file
     */
    private List<Cpe> processXML(final File xml) throws UpdateException {
        try {
            final SAXParserFactory factory = SAXParserFactory.newInstance();
            final SAXParser saxParser = factory.newSAXParser();
            final CPEHandler handler = new CPEHandler();
            saxParser.parse(xml, handler);
            return handler.getData();
        } catch (ParserConfigurationException ex) {
            throw new UpdateException("Unable to parse CPE XML file due to SAX Parser Issue", ex);
        } catch (SAXException ex) {
            throw new UpdateException("Unable to parse CPE XML file due to SAX Parser Exception", ex);
        } catch (IOException ex) {
            throw new UpdateException("Unable to parse CPE XML file due to IO Failure", ex);
        }
    }

    /**
     * Checks to find the last time the CPE data was refreshed and if it needs to be updated.
     *
     * @return true if the CPE data should be refreshed
     */
    private boolean updateNeeded() {
        final Date now = new Date();
        final int days = Settings.getInt(Settings.KEYS.CVE_MODIFIED_VALID_FOR_DAYS, 30);
        long timestamp = 0;
        final String ts = getProperties().getProperty(LAST_CPE_UPDATE);
        if (ts != null && ts.matches("^[0-9]+$")) {
            timestamp = Long.parseLong(ts);
        }
        return !DateUtil.withinDateRange(timestamp, now.getTime(), days);
    }

    /**
     * Extracts the file contained in a gzip archive. The extracted file is placed in the exact same path as the file specified.
     *
     * @param file the archive file
     * @throws FileNotFoundException thrown if the file does not exist
     * @throws IOException thrown if there is an error extracting the file.
     */
    private void extractGzip(File file) throws FileNotFoundException, IOException {
        //TODO - move this to a util class as it is duplicative of (copy of) code in the DownloadTask
        final String originalPath = file.getPath();
        final File gzip = new File(originalPath + ".gz");
        if (gzip.isFile() && !gzip.delete()) {
            gzip.deleteOnExit();
        }
        if (!file.renameTo(gzip)) {
            throw new IOException("Unable to rename '" + file.getPath() + "'");
        }
        final File newfile = new File(originalPath);

        final byte[] buffer = new byte[4096];

        GZIPInputStream cin = null;
        FileOutputStream out = null;
        try {
            cin = new GZIPInputStream(new FileInputStream(gzip));
            out = new FileOutputStream(newfile);

            int len;
            while ((len = cin.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } finally {
            if (cin != null) {
                try {
                    cin.close();
                } catch (IOException ex) {
                    LOGGER.trace("ignore", ex);
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ex) {
                    LOGGER.trace("ignore", ex);
                }
            }
            if (gzip.isFile()) {
                FileUtils.deleteQuietly(gzip);
            }
        }
    }
}
