org.eclipse.packagedrone.repo.channel.apm.store.BlobStore.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.packagedrone.repo.channel.apm.store.BlobStore.java

Source

/*******************************************************************************
 * Copyright (c) 2015 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.repo.channel.apm.store;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.packagedrone.utils.io.IOConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.ByteStreams;
import com.google.common.io.CountingOutputStream;

public class BlobStore implements Closeable {
    private final static Logger logger = LoggerFactory.getLogger(BlobStore.TransactionImpl.class);

    public class TransactionImpl implements Transaction {
        private boolean done;

        private final Set<String> deleted = new HashSet<>();

        private final Set<String> added = new HashSet<>();

        @Override
        public boolean delete(final String id) throws IOException {
            testDone();

            final boolean existed = BlobStore.this.currentIndex.contains(id) || this.added.contains(id);

            this.added.remove(id);
            this.deleted.add(id);

            return existed;
        }

        @Override
        public long create(final String id, final IOConsumer<OutputStream> consumer) throws IOException {
            testDone();

            final long size = handleCreate(id, consumer);

            this.added.add(id);
            this.deleted.remove(id);

            return size;
        }

        @Override
        public boolean stream(final String id, final IOConsumer<InputStream> consumer) throws IOException {
            testDone();

            if (this.deleted.contains(id)) {
                return false;
            }

            return handleStream(id, consumer);
        }

        @Override
        public void commit() {
            testDone();
            this.done = true;

            handleCommit(this, this.deleted, this.added);
        }

        @Override
        public void rollback() {
            testDone();
            this.done = true;

            handleRollback(this);
        }

        protected void testDone() {
            if (this.done) {
                throw new IllegalStateException("Transaction already closed");
            }
        }
    }

    public static interface Transaction {
        public boolean delete(String id) throws IOException;

        public default long create(final String id, final InputStream source) throws IOException {
            return create(id, target -> ByteStreams.copy(source, target));
        }

        public long create(String id, IOConsumer<OutputStream> consumer) throws IOException;

        public boolean stream(String id, IOConsumer<InputStream> consumer) throws IOException;

        public void commit();

        public void rollback();
    }

    private final Path base;

    private final Path dataPath;

    private Transaction transaction;

    private Set<String> currentIndex;

    private final Path indexPath;

    public BlobStore(final Path path) throws IOException {
        this.base = path;

        this.dataPath = this.base.resolve("data").toAbsolutePath();
        this.indexPath = this.base.resolve("index.txt").toAbsolutePath();

        Files.createDirectories(this.dataPath);

        Set<String> index = loadIndex();
        if (index == null) {
            // build index
            index = scanIndex(path);
            writeIndex(index);
        }
        this.currentIndex = index;
    }

    public static Set<String> scanIndex(final Path basePath) throws IOException {
        try {
            return Files.walk(basePath.resolve("data")).filter(Files::isRegularFile)
                    .map(path -> path.getName(path.getNameCount() - 1).toString()).collect(Collectors.toSet());
        } catch (final NoSuchFileException e) {
            return Collections.emptySet();
        }
    }

    public boolean handleStream(final String id, final IOConsumer<InputStream> consumer) throws IOException {
        return processStream(id, consumer);
    }

    public long handleCreate(final String id, final IOConsumer<OutputStream> consumer) throws IOException {
        final Path path = makeDataPath(id);

        Files.createDirectories(path.getParent());

        try (CountingOutputStream stream = new CountingOutputStream(
                new BufferedOutputStream(Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)))) {
            consumer.accept(stream);
            return stream.getCount();
        }
    }

    @Override
    public synchronized void close() {
        if (this.transaction != null) {
            handleRollback(this.transaction);
        }
    }

    public boolean stream(final String id, final IOConsumer<InputStream> consumer) throws IOException {
        if (!this.currentIndex.contains(id)) {
            // FIXME: check locking
            return false;
        }
        return processStream(id, consumer);
    }

    private boolean processStream(final String id, final IOConsumer<InputStream> consumer) throws IOException {
        final Path path = makeDataPath(id);

        try (InputStream stream = new BufferedInputStream(Files.newInputStream(path, StandardOpenOption.READ))) {
            consumer.accept(stream);
            return true;
        } catch (final NoSuchFileException e) {
            return false;
        }
    }

    public synchronized Transaction start() {
        if (this.transaction == null) {
            this.transaction = new TransactionImpl();
            return this.transaction;
        }

        throw new IllegalStateException("Transaction already in progress");
    }

    protected synchronized void handleCommit(final Transaction transaction, final Set<String> deleted,
            final Set<String> added) {
        if (this.transaction != transaction) {
            throw new IllegalStateException("Invalid transaction");
        }
        this.transaction = null;

        try {
            final Set<String> index = new HashSet<>(this.currentIndex);
            index.removeAll(deleted);
            index.addAll(added);

            writeIndex(index);

            for (final String id : deleted) {
                Files.deleteIfExists(makeDataPath(id));
            }
        } catch (final IOException e) {
            throw new RuntimeException("Failed to commit", e);
        }
    }

    private void writeIndex(final Set<String> index) throws IOException {
        Files.write(this.indexPath, index, StandardCharsets.UTF_8);
        this.currentIndex = index;
    }

    private Set<String> loadIndex() throws IOException {
        try {
            return new HashSet<>(Files.readAllLines(this.indexPath, StandardCharsets.UTF_8));
        } catch (final NoSuchFileException e) {
            return null;
        }
    }

    protected synchronized void handleRollback(final Transaction transaction) {
        if (this.transaction != transaction) {
            throw new IllegalStateException("Invalid transaction");
        }
        this.transaction = null;

        try {
            vacuum();
        } catch (final IOException e) {
            logger.warn("Failed to vacuum", e);
        }
    }

    public synchronized void vacuum() throws IOException {
        final Set<String> ids = scanIndex(this.base);
        ids.removeAll(this.currentIndex);

        for (final String id : ids) {
            final Path path = makeDataPath(id);
            logger.debug("Vacuuming file: {}", path);
            Files.deleteIfExists(path);
        }
    }

    private Path makeDataPath(final String id) {
        final String l1 = id.substring(0, 1);
        final String l2 = id.substring(1, 2);
        return this.dataPath.resolve(Paths.get(l1, l2, id));
    }

}