ee.ria.xroad.common.messagelog.archive.LogArchiveWriter.java Source code

Java tutorial

Introduction

Here is the source code for ee.ria.xroad.common.messagelog.archive.LogArchiveWriter.java

Source

/**
 * The MIT License
 * Copyright (c) 2015 Estonian Information System Authority (RIA), Population Register Centre (VRK)
 *
 * 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 ee.ria.xroad.common.messagelog.archive;

import static ee.ria.xroad.common.DefaultFilepaths.createTempFile;
import static ee.ria.xroad.common.messagelog.MessageLogProperties.getArchivePath;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;

import ee.ria.xroad.common.messagelog.LogRecord;
import ee.ria.xroad.common.messagelog.MessageLogProperties;
import ee.ria.xroad.common.messagelog.MessageRecord;
import lombok.extern.slf4j.Slf4j;

/**
 * Class for writing log records to zip file containing ASiC containers
 * (archive).
 */
@Slf4j
public class LogArchiveWriter implements Closeable {

    private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");

    public static final int MAX_RANDOM_GEN_ATTEMPTS = 1000;

    private static final int RANDOM_LENGTH = 10;

    private final Path outputPath;
    private final LogArchiveBase archiveBase;

    private final LinkingInfoBuilder linkingInfoBuilder;
    private final LogArchiveCache logArchiveCache;

    protected final Charset charset = Charset.forName(StandardCharsets.UTF_8.name());

    protected WritableByteChannel archiveOut;

    private Path archiveTmp;
    private Path lastHashStepTmp;

    /**
     * Creates new LogArchiveWriter
     * @param outputPath directory where the log archive is created.
     * @param workingPath directory where the temporary files are stored
     * @param archiveBase interface to archive database.
     */
    public LogArchiveWriter(Path outputPath, Path workingPath, LogArchiveBase archiveBase) {
        this.outputPath = outputPath;
        this.archiveBase = archiveBase;

        this.linkingInfoBuilder = new LinkingInfoBuilder(MessageLogProperties.getHashAlg(), archiveBase);

        this.logArchiveCache = new LogArchiveCache(LogArchiveWriter::generateRandom, linkingInfoBuilder,
                workingPath);
    }

    /**
     * Write a message log record.
     * @param logRecord the log record
     * @throws Exception in case of any errors
     */
    public void write(LogRecord logRecord) throws Exception {
        if (logRecord == null) {
            throw new IllegalArgumentException("log record must not be null");
        }

        if (archiveOut == null) {
            initOutput();
        }

        log.trace("write({})", logRecord.getId());

        if (logRecord instanceof MessageRecord) {
            logArchiveCache.add((MessageRecord) logRecord);
        }

        archiveBase.markRecordArchived(logRecord);

        if (logArchiveCache.isRotating()) {
            rotate();
        }
    }

    @Override
    public void close() throws IOException {
        log.trace("Closing log archive writer ...");

        try {
            if (archiveAsicContainers()) {
                closeOutputs();

                saveArchive();

                logArchiveCache.close();
            }
        } finally {
            clearTempArchive();
        }
    }

    private void clearTempArchive() {
        if (archiveTmp == null) {
            return;
        }

        // Without it, temp file remains on the disk even after closing.
        deleteQuietly(archiveTmp.toFile());
        deleteQuietly(lastHashStepTmp.toFile());

        archiveTmp = null;
        lastHashStepTmp = null;
    }

    protected WritableByteChannel createArchiveOutput() throws Exception {
        archiveTmp = createTempFile(outputPath, "mlogtmp", null);
        lastHashStepTmp = createTempFile(outputPath, "lasthashsteptmp", null);

        return createOutputToTempFile(archiveTmp);
    }

    protected String getArchiveFilename(String random) {
        return String.format("mlog-%s-%s-%s.zip", simpleDateFormat.format(logArchiveCache.getStartTime()),
                simpleDateFormat.format(logArchiveCache.getEndTime()), random);
    }

    protected void rotate() throws Exception {
        log.trace("rotate()");
        archiveAsicContainers();

        closeOutputs();
        archiveOut = null;

        saveArchive();

        archiveTmp = null;
        lastHashStepTmp = null;
    }

    private boolean archiveAsicContainers() {
        try (InputStream input = logArchiveCache.getArchiveFile();
                OutputStream output = Channels.newOutputStream(archiveOut)) {
            IOUtils.copy(input, output);
        } catch (IOException e) {
            log.error("Failed to archive ASiC containers due to IO error", e);
            return false;
        }

        return true;
    }

    private void closeOutputs() throws IOException {
        if (archiveOut != null) {
            archiveOut.close();
        }
    }

    private void initOutput() throws Exception {
        log.trace("initOutput()");

        try {
            closeOutputs();
        } catch (Exception e) {
            log.trace("Failed to close output files", e);
        }

        archiveOut = createArchiveOutput();
    }

    private void saveArchive() throws IOException {
        if (archiveTmp == null) {
            return;
        }

        String archiveFilename = getArchiveFilename(generateRandom());

        Path archiveFile = outputPath.resolve(archiveFilename);

        atomicMove(archiveTmp, archiveFile);

        setArchivedInDatabase(archiveFilename);

        linkingInfoBuilder.afterArchiveSaved();

        log.info("Created archive file {}", archiveFile);
    }

    private void setArchivedInDatabase(String archiveFilename) throws IOException {
        try {
            archiveBase.markArchiveCreated(
                    new DigestEntry(linkingInfoBuilder.getCreatedArchiveLastDigest(), archiveFilename));
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    private static String generateRandom() {
        String random = randomAlphanumeric(RANDOM_LENGTH);

        int attempts = 0;
        while (!filenameRandomUnique(random)) {
            if (++attempts > MAX_RANDOM_GEN_ATTEMPTS) {
                throw new RuntimeException(
                        "Could not generate unique random in " + MAX_RANDOM_GEN_ATTEMPTS + " attempts");
            }

            random = randomAlphanumeric(RANDOM_LENGTH);
        }

        return random;
    }

    private static boolean filenameRandomUnique(String random) {
        String filenameEnd = String.format("-%s.zip", random);

        String[] fileNamesWithSameRandom = new File(getArchivePath())
                .list((file, name) -> name.startsWith("mlog-") && name.endsWith(filenameEnd));

        return ArrayUtils.isEmpty(fileNamesWithSameRandom);
    }

    private static WritableByteChannel createOutputToTempFile(Path tmp) throws Exception {
        return Files.newByteChannel(tmp, CREATE, WRITE, TRUNCATE_EXISTING);
    }

    private static void atomicMove(Path source, Path destination) throws IOException {
        Files.move(source, destination, REPLACE_EXISTING, ATOMIC_MOVE);
    }
}