de.ncoder.studipsync.Syncer.java Source code

Java tutorial

Introduction

Here is the source code for de.ncoder.studipsync.Syncer.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Niko Fink
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package de.ncoder.studipsync;

import de.ncoder.studipsync.data.Download;
import de.ncoder.studipsync.data.Seminar;
import de.ncoder.studipsync.storage.Storage;
import de.ncoder.studipsync.studip.StudipAdapter;
import de.ncoder.studipsync.studip.StudipException;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import static de.ncoder.studipsync.studip.StudipAdapter.PAGE_DOWNLOADS;
import static de.ncoder.studipsync.studip.StudipAdapter.PAGE_DOWNLOADS_LATEST;

public class Syncer {
    private static final Logger log = LoggerFactory.getLogger(Syncer.class);

    private final StudipAdapter adapter;
    private final Storage storage;
    private final ReentrantLock browserLock = new ReentrantLock();
    private Marker marker;
    private CheckLevel checkLevel;

    public Syncer(StudipAdapter adapter, Storage storage) {
        this.adapter = adapter;
        this.storage = storage;
    }

    public void init() throws StudipException {
        browserLock.lock();
        try {
            adapter.init();
            adapter.doLogin();
        } finally {
            browserLock.unlock();
        }
    }

    public void close() throws IOException {
        browserLock.lock();
        try {
            adapter.close();
        } finally {
            browserLock.unlock();
        }
        storage.close();
    }

    public synchronized void sync() throws StudipException, InterruptedException {
        final List<Seminar> seminars;

        //Access seminars
        browserLock.lock();
        try {
            init();
            seminars = getSeminars();
        } finally {
            browserLock.unlock();
        }
        log.info(seminars.size() + " seminars");

        //Sync seminars
        sync(seminars);
    }

    public synchronized void sync(List<Seminar> seminars) throws StudipException, InterruptedException {
        List<StudipException> exceptions = new ArrayList<>();
        for (Seminar seminar : seminars) {
            marker = MarkerFactory.getMarker(seminar.getID());
            try {
                syncSeminar(seminar, false);
            } catch (StudipException e) {
                log.error(marker, "Couldn't synchronize", e);
                exceptions.add(e);
            }
        }

        if (!exceptions.isEmpty()) {
            StudipException ex = new StudipException("Not all seminars are in sync");
            for (StudipException suppressed : exceptions) {
                ex.addSuppressed(suppressed);
            }
            throw ex;
        }
    }

    public void syncSeminar(final Seminar seminar, boolean forceAbsolute) throws StudipException {
        try {
            //Find downloads
            log.info(marker, seminar.getFullName() + (forceAbsolute ? ", absolute" : ""));
            List<Download> downloads = getDownloads(seminar);
            log.info(marker,
                    "\tFound " + downloads.size() + " downloadable file" + (downloads.size() != 1 ? "s" : ""));
            boolean wasAbsolute = syncDownloads(downloads, forceAbsolute);

            //Check downloads
            checkSeminar(seminar, wasAbsolute || forceAbsolute);
        } catch (IOException e) {
            throw new StudipException("Could not synchronize Seminar " + seminar + ".", e);
        } catch (StudipException ex) {
            ex.put("download.seminar", seminar);
            ex.put("download.forceAbsolute", forceAbsolute);
            throw ex;
        }
    }

    /**
     * @return wasAbsolute, true if at every download was absolutely synchronized
     */
    public boolean syncDownloads(List<Download> downloads, boolean forceAbsolute) throws StudipException {
        boolean wasAbsolute = true;
        for (final Download download : downloads) {
            if (download.getLevel() == 0) {
                try {
                    final boolean downloadDiff;
                    final InputStream src;
                    if (forceAbsolute) {
                        //Absolute forced
                        downloadDiff = false;
                        src = startDownload(download, false);
                        log.info(marker, "\tabs: " + download.getFileName());
                    } else if (download.isChanged()) {
                        //Changed data
                        downloadDiff = true;
                        src = startDownload(download, true);
                        log.info(marker, "\tpar: " + download.getFileName());
                    } else {
                        //Nothing changed
                        downloadDiff = true;
                        src = null;
                        log.info(marker, "\tign: " + download.getFileName());
                    }
                    if (src != null) {
                        storage.store(download, src, downloadDiff);
                    }
                    if (downloadDiff) {
                        wasAbsolute = false;
                    }
                } catch (IOException e) {
                    log.warn(marker, "Couldn't download " + download, e);
                    wasAbsolute = false;
                }
            }
        }
        return wasAbsolute;
    }

    public void checkSeminar(Seminar seminar, boolean syncWasAbsolute) throws IOException, StudipException {
        if (!isSeminarInSync(seminar)) {
            log.info(marker, "NOT IN-SYNC");
            if (syncWasAbsolute) {
                throw new StudipException("Could not synchronize Seminar " + seminar
                        + ". Local data is different from online data after full download.");
            } else {
                syncSeminar(seminar, true);
            }
        } else {
            log.info(marker, "FINISHED " + seminar.getName());
        }
    }

    public boolean isSeminarInSync(Seminar seminar) throws IOException, StudipException {
        if (!checkLevel.includes(CheckLevel.Count)) {
            return true;
        }

        //List downloads
        final List<Download> downloads;
        browserLock.lock();
        try {
            downloads = adapter.parseDownloads(PAGE_DOWNLOADS_LATEST, false);
        } finally {
            browserLock.unlock();
        }
        if (downloads.isEmpty()) {
            //No downloads - nothing to do
            return true;
        }

        //List local files
        final List<Path> localFiles = new LinkedList<>();
        final Path storagePath = storage.resolve(seminar);
        if (!Files.exists(storagePath)) {
            //No local files despite available downloads
            log.info(marker, "Seminar is empty!");
            return false;
        }
        Files.walkFileTree(storagePath, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                localFiles.add(file);
                return FileVisitResult.CONTINUE;
            }
        });

        //Count local files
        if (localFiles.size() < downloads.size()) {
            // Missing files
            log.warn(marker, "Seminar has only " + localFiles.size() + " local file(s) of " + downloads.size()
                    + " online file(s).");
            return false;
        } else {
            // Ignore surplus files
            log.debug(marker, "Seminar has deleted file(s) left! " + localFiles.size() + " local file(s) and "
                    + downloads.size() + " online file(s).");
        }

        //Check local files
        if (!areFilesInSync(downloads, localFiles)) {
            return false;
        }

        return true;
    }

    public boolean areFilesInSync(List<Download> downloads, List<Path> localFiles) throws IOException {
        if (!checkLevel.includes(CheckLevel.Files)) {
            return true;
        }
        for (Download download : downloads) {
            //Find matching candidates
            List<Path> localCandidates = new LinkedList<>();
            for (Path local : localFiles) {
                // Check candidate name
                if (local.getName(local.getNameCount() - 1).toString().equals(download.getFileName())) {
                    if (!localCandidates.isEmpty()) {
                        //Already found a candidate
                        log.debug(marker,
                                "Local files " + localCandidates + " and " + local + " match " + download + "!");
                    }
                    localCandidates.add(local);

                    //Check LastModifiedTime
                    if (!checkLevel.includes(CheckLevel.ModTime)) {
                        continue;
                    }
                    Date localLastMod = new Date(Files.getLastModifiedTime(local).toMillis());
                    if (!localLastMod.after(download.getLastModified())) {
                        //Candidate *potentially* outdated
                        log.warn(marker, "Local file " + local + "(" + localLastMod + ") older than online Version "
                                + download + "(" + download.getLastModified() + ")!");
                        return false;
                    }
                }
            }

            //Require at least one candidate
            if (localCandidates.isEmpty()) {
                //No candidates found
                log.warn(marker, "No local file matching " + download + " (~" + download.getFileName() + ")!");
                return false;
            }
        }
        return true;
    }

    public List<Seminar> getSeminars() throws StudipException {
        browserLock.lock();
        try {
            return adapter.parseSeminars();
        } finally {
            browserLock.unlock();
        }
    }

    public List<Download> getDownloads(Seminar seminar) throws StudipException {
        browserLock.lock();
        try {
            adapter.selectSeminar(seminar);
            return adapter.parseDownloads(PAGE_DOWNLOADS, true);
        } finally {
            browserLock.unlock();
        }
    }

    public InputStream startDownload(Download download, boolean diffOnly) throws StudipException, IOException {
        browserLock.lock();
        try {
            return adapter.startDownload(download, diffOnly);
        } finally {
            browserLock.unlock();
        }
    }

    public CheckLevel getCheckLevel() {
        return checkLevel;
    }

    public void setCheckLevel(CheckLevel checkLevel) {
        this.checkLevel = checkLevel;
    }

    public static enum CheckLevel implements Comparable<CheckLevel> {
        None, Count, Files, ModTime, All;

        public static CheckLevel Default = All;

        public static CheckLevel get(String level) throws ParseException {
            if (level != null) {
                try {
                    return valueOf(level);
                } catch (IllegalArgumentException earg) {
                    try {
                        return CheckLevel.values()[Integer.parseInt(level)];
                    } catch (NumberFormatException enumb) {
                        ParseException pe = new ParseException(level + " is not a CheckLevel.");
                        pe.initCause(earg);
                        pe.addSuppressed(enumb);
                        throw pe;
                    }
                }
            } else {
                return Default;
            }
        }

        public boolean includes(CheckLevel other) {
            return compareTo(other) >= 0;
        }
    }

    public StudipAdapter getAdapter() {
        return adapter;
    }

    public Storage getStorage() {
        return storage;
    }
}