org.openthinclient.pkgmgr.UpdateDatabase.java Source code

Java tutorial

Introduction

Here is the source code for org.openthinclient.pkgmgr.UpdateDatabase.java

Source

/*******************************************************************************
 * openthinclient.org ThinClient suite <p/> Copyright (C) 2004, 2007 levigo holding GmbH. All Rights
 * Reserved. <p/> <p/> This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version. <p/> 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 for
 * more details. <p/> You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 ******************************************************************************/
package org.openthinclient.pkgmgr;

import org.apache.commons.lang3.StringUtils;
import org.openthinclient.manager.util.http.DownloadManager;
import org.openthinclient.manager.util.http.config.NetworkConfiguration;
import org.openthinclient.pkgmgr.connect.PackageListDownloader;
import org.openthinclient.pkgmgr.db.Package;
import org.openthinclient.pkgmgr.db.PackageManagerDatabase;
import org.openthinclient.pkgmgr.db.Source;
import org.openthinclient.pkgmgr.op.PackageListUpdateReport;
import org.openthinclient.progress.ListenableProgressFuture;
import org.openthinclient.progress.ProgressReceiver;
import org.openthinclient.progress.ProgressTask;
import org.openthinclient.util.dpkg.LocalPackageList;
import org.openthinclient.util.dpkg.PackagesListParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UpdateDatabase implements ProgressTask<PackageListUpdateReport> {

    private static final Logger LOG = LoggerFactory.getLogger(UpdateDatabase.class);
    private final PackageManagerConfiguration configuration;
    private final SourcesList sourcesList;
    private final PackageManagerDatabase db;
    private final PackageManagerDirectoryStructure directoryStructure;
    private final DownloadManager downloadManager;

    public UpdateDatabase(PackageManagerConfiguration configuration, SourcesList sourcesList,
            PackageManagerDatabase db, DownloadManager downloadManager) {
        this.configuration = configuration;
        this.sourcesList = sourcesList;
        this.db = db;
        this.directoryStructure = new PackageManagerDirectoryStructureImpl(configuration);
        this.downloadManager = downloadManager;
    }

    private static URL createPackageChangeLogURL(Package pkg) {
        try {
            URL serverPath = pkg.getSource().getUrl();
            if (!serverPath.toExternalForm().endsWith("/")) {
                serverPath = new URL(serverPath.toExternalForm() + "/");
            }

            return new URL(serverPath, pkg.getName() + ".changelog");
        } catch (MalformedURLException e) {
            throw new RuntimeException("Failed to access changelog due to illegal url", e);
        }
    }

    private boolean downloadChangelogFile(NetworkConfiguration.ProxyConfiguration proxyConfiguration, Source source,
            Package pkg, PackageManagerTaskSummary taskSummary) throws PackageManagerException {
        try {
            final Path changelogFile = directoryStructure.changelogFileLocation(source, pkg);
            Files.createDirectories(changelogFile.getParent());
            downloadManager.downloadTo(createPackageChangeLogURL(pkg), changelogFile.toFile());
            return true;
        } catch (final Exception e) {
            if (null != taskSummary) {
                taskSummary.addWarning(e.toString());
            }
            LOG.warn("Changelog download failed for package " + pkg.getName());
            return false;
        }
    }

    private void processPackage(Source source, Package updatedPkg, PackageListUpdateReport report) {

        final Package existing = db.getPackageRepository().getBySourceAndNameAndVersion(source,
                updatedPkg.getName(), updatedPkg.getVersion());

        if (existing != null) {
            if (existing.equals(updatedPkg)) {
                LOG.info("Skipping already existing and equal {}", updatedPkg.toStringWithNameAndVersion());
                report.incSkipped();
            } else {
                LOG.info("Updating the package metadata for: {}", existing.toStringWithNameAndVersion());
                // update the package metadata
                existing.updateFrom(updatedPkg);
                // after applying, download and attach the changelog. This must be called after updateFrom
                // to ensure that the downloaded changelog will not be overridden by the updateFrom logic
                existing.setChangeLog(extractChangelogEntries(source, updatedPkg));
                db.getPackageRepository().save(existing);
                report.incUpdated();
            }
        } else {
            // get changelog for new package
            updatedPkg.setChangeLog(extractChangelogEntries(source, updatedPkg));

            LOG.info("Adding new package {}", updatedPkg.toStringWithNameAndVersion());
            db.getPackageRepository().save(updatedPkg);
            report.incAdded();
        }

    }

    /**
     * Debian changelog format: <a href="https://www.debian.org/doc/debian-policy/ch-source.html">Changelog Format Specification</a>
     *
     * @param source the source
     * @param pkg    the package
     * @return Changelog entries as String
     */
    private String extractChangelogEntries(Source source, Package pkg) {

        LOG.info("Extract the Changelog for {}", pkg.toStringWithNameAndVersion());

        PackageManagerTaskSummary taskSummary = new PackageManagerTaskSummary();
        downloadChangelogFile(configuration.getProxyConfiguration(), source, pkg, taskSummary);
        LOG.trace("taskSummary for downloadChangelogFile: {}", taskSummary.getWarnings());

        // Regarding to debian-policy: the package-version is set in brackets, i.e. package: zonk (2.0-1)
        String nameAndVersion = pkg.getName() + " (" + pkg.getDisplayVersion() + ")";
        List<String> lines = parseChangelogFile(source, pkg);
        StringBuilder sb = new StringBuilder();
        boolean addLines = false;
        for (String line : lines) {
            if (line.toLowerCase().contains(nameAndVersion.toLowerCase())) {
                addLines = true;
            }
            if (addLines && StringUtils.isNotBlank(line)) {
                sb.append(line).append("\n");
            }
        }
        return sb.toString();
    }

    /**
     * Parse the changelog file
     *
     * @param source the package source
     * @param pkg    the package
     * @return a list with parsed lines
     */
    private List<String> parseChangelogFile(Source source, Package pkg) {
        try {
            Path changelogFile = directoryStructure.changelogFileLocation(source, pkg);
            LOG.trace("changelogFile: {}", changelogFile);
            return Files.lines(changelogFile).collect(Collectors.toList());
        } catch (IOException e) {
            LOG.error("Cannot read changelogFile for package " + pkg.toStringWithNameAndVersion());
            return Collections.emptyList();
        }
    }

    private Stream<Package> parsePackagesList(LocalPackageList localPackageList) {
        LOG.info("Processing packages for {}", localPackageList.getSource().getUrl());

        try {
            return new PackagesListParser().parse(Files.newInputStream(localPackageList.getPackagesFile().toPath()))
                    .stream().map(p -> {
                        p.setSource(localPackageList.getSource());
                        return p;
                    });
        } catch (IOException e) {
            LOG.error("Failed to parse packages list for " + localPackageList.getSource().getUrl(), e);
            return Stream.empty();
        }
    }

    @Override
    public ProgressTaskDescription getDescription(Locale locale) {
        // FIXME
        return null;
    }

    @Override
    public PackageListUpdateReport execute(ProgressReceiver progressReceiver) {

        final PackageListDownloader packageListDownloader = new PackageListDownloader(configuration,
                downloadManager);

        final PackageListUpdateReport report = new PackageListUpdateReport();

        for (Source source : sourcesList.getSources()) {

            if (!source.isEnabled()) {
                LOG.info("Disabled source {} skipped.", source);
                continue;
            }

            updateProgress(progressReceiver, source);

            final LocalPackageList localPackageList = packageListDownloader.download(source, progressReceiver);
            List<Package> parsePackagesList = parsePackagesList(localPackageList).collect(Collectors.toList());
            parsePackagesList.forEach((pkg) -> processPackage(source, pkg, report));

            List<Package> outdatedPackages = collectOutdatedPackages(source, parsePackagesList);
            outdatedPackages.forEach(existingPkg -> {
                if (existingPkg.isInstalled()) {
                    LOG.warn("Keep existing {} installed, but {} doesn't provide it anymore.",
                            existingPkg.toStringWithNameAndVersion(), source);
                } else {
                    LOG.info("Deleting existing {}, because {} doesn't provide it anymore.",
                            existingPkg.toStringWithNameAndVersion(), source);
                    db.getPackageRepository().delete(existingPkg);
                    report.incRemoved();
                }
            });

            // update the timestamp
            source.setLastUpdated(LocalDateTime.now());
            db.getSourceRepository().save(source);
        }
        return report;
    }

    protected List<Package> collectOutdatedPackages(Source source, List<Package> newPackageList) {

        // get (already updated) package-list and remove outdated packages, disable installed outdated packages
        List<Package> existingPackages = db.getPackageRepository().findBySource(source);

        return collectOutdatedPackages(newPackageList, existingPackages);

    }

    protected List<Package> collectOutdatedPackages(List<Package> newPackageList, List<Package> existingPackages) {
        return existingPackages.stream() //
                .filter(p1 -> newPackageList.stream().noneMatch(p2 -> packageMetadataEquals(p1, p2)))
                .collect(Collectors.toList());
    }

    protected boolean packageMetadataEquals(Package p1, Package p2) {

        return Objects.equals(p1.getSource(), p2.getSource()) && Objects.equals(p1.getName(), p2.getName())
                && Objects.equals(p1.getVersion(), p2.getVersion());
    }

    private void updateProgress(ProgressReceiver progressReceiver, Source currentSource) {

        final List<Source> sources = sourcesList.getSources();
        final int sourceCount = sources.size();
        final boolean multipleSources = sourceCount != 1;

        final String message = "Updating " + currentSource.getUrl();
        final double progress;
        if (multipleSources) {
            progress = (double) sources.indexOf(currentSource) * (1d / (double) sourceCount);
        } else {
            progress = ListenableProgressFuture.INDETERMINATE;
        }
        progressReceiver.progress(message, progress);
    }
}