org.trellisldp.file.FileBinaryService.java Source code

Java tutorial

Introduction

Here is the source code for org.trellisldp.file.FileBinaryService.java

Source

/*
 * 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.
 */
package org.trellisldp.file;

import static java.nio.file.Files.copy;
import static java.nio.file.Files.delete;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.of;
import static java.util.ServiceLoader.load;
import static java.util.concurrent.CompletableFuture.supplyAsync;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.codec.digest.DigestUtils.updateDigest;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD2;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD5;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA3_256;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA3_384;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA3_512;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_1;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_256;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_384;
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_512;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.security.MessageDigest;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Supplier;

import javax.inject.Inject;

import org.apache.commons.rdf.api.IRI;
import org.apache.tamaya.Configuration;
import org.apache.tamaya.ConfigurationProvider;
import org.slf4j.Logger;
import org.trellisldp.api.Binary;
import org.trellisldp.api.BinaryMetadata;
import org.trellisldp.api.BinaryService;
import org.trellisldp.api.DefaultIdentifierService;
import org.trellisldp.api.IdentifierService;

/**
 * A {@link BinaryService} implementation that stores LDP-NR resources as files on a local filesystem.
 *
 * <p>This service supports the following digest algorithms:
 * <ul>
 * <li>MD5</li>
 * <li>MD2</li>
 * <li>SHA</li>
 * <li>SHA-1</li>
 * <li>SHA-256</li>
 * <li>SHA-384</li>
 * <li>SHA-512</li>
 * </ul>
 *
 * <p>When running under JDK 9+, the following additional digest algorithms are supported:
 * <ul>
 * <li>SHA3-256</li>
 * <li>SHA3-384</li>
 * <li>SHA3-512</li>
 * </ul>
 */
public class FileBinaryService implements BinaryService {

    /** The configuration key controlling the base filesystem path for the binary service. */
    public static final String CONFIG_FILE_BINARY_BASE_PATH = "trellis.file.binary.basepath";

    /** The configuration key controlling the levels of hierarchy in a binary storage layout. */
    public static final String CONFIG_FILE_BINARY_HIERARCHY = "trellis.file.binary.hierarchy";

    /** The configuration key controlling the length of each level of hierarchy in a filesystem layout. */
    public static final String CONFIG_FILE_BINARY_LENGTH = "trellis.file.binary.length";

    private static final Logger LOGGER = getLogger(FileBinaryService.class);
    private static final String SHA = "SHA";
    private static final int DEFAULT_HIERARCHY = 3;
    private static final int DEFAULT_LENGTH = 2;

    private static final Set<String> algorithms = asList(MD5, MD2, SHA, SHA_1, SHA_256, SHA_384, SHA_512, SHA3_256,
            SHA3_384, SHA3_512).stream().collect(toSet());

    private final String basePath;
    private final Supplier<String> idSupplier;

    /**
     * Create a File-based Binary service.
     */
    @Inject
    public FileBinaryService() {
        this(of(load(IdentifierService.class)).map(ServiceLoader::iterator).filter(Iterator::hasNext)
                .map(Iterator::next).orElseGet(DefaultIdentifierService::new));
    }

    /**
     * Create a File-based Binary service.
     *
     * @param idService an identifier service
     */
    public FileBinaryService(final IdentifierService idService) {
        this(idService, ConfigurationProvider.getConfiguration());
    }

    /**
     * Create a File-based Binary service.
     *
     * @param idService an identifier service
     * @param basePath the base file path
     * @param hierarchy the levels of hierarchy
     * @param length the length of each level of hierarchy
     */
    public FileBinaryService(final IdentifierService idService, final String basePath, final int hierarchy,
            final int length) {
        this.basePath = requireNonNull(basePath, CONFIG_FILE_BINARY_BASE_PATH + " configuration may not be null!");
        LOGGER.info("Storing binaries as files at {}", basePath);
        this.idSupplier = idService.getSupplier("file:///", hierarchy, length);
    }

    private FileBinaryService(final IdentifierService idService, final Configuration config) {
        this(idService, config.get(CONFIG_FILE_BINARY_BASE_PATH),
                config.getOrDefault(CONFIG_FILE_BINARY_HIERARCHY, Integer.class, DEFAULT_HIERARCHY),
                config.getOrDefault(CONFIG_FILE_BINARY_LENGTH, Integer.class, DEFAULT_LENGTH));
    }

    @Override
    public CompletionStage<Binary> get(final IRI identifier) {
        return supplyAsync(() -> new FileBinary(getFileFromIdentifier(identifier)));
    }

    @Override
    public CompletionStage<Void> purgeContent(final IRI identifier) {
        return supplyAsync(() -> {
            try {
                delete(getFileFromIdentifier(identifier).toPath());
            } catch (final IOException ex) {
                LOGGER.warn("File could not deleted {}: {}", identifier, ex.getMessage());
            }
            return null;
        });
    }

    @Override
    public CompletionStage<Void> setContent(final BinaryMetadata metadata, final InputStream stream) {
        requireNonNull(stream, "InputStream may not be null!");
        return supplyAsync(() -> {
            final File file = getFileFromIdentifier(metadata.getIdentifier());
            LOGGER.debug("Setting binary content for {} at {}", metadata.getIdentifier(), file.getAbsolutePath());
            try (final InputStream input = stream) {
                final File parent = file.getParentFile();
                parent.mkdirs();
                copy(stream, file.toPath(), REPLACE_EXISTING);
            } catch (final IOException ex) {
                throw new UncheckedIOException("Error while setting content for " + metadata.getIdentifier(), ex);
            }
            return null;
        });
    }

    @Override
    public CompletionStage<MessageDigest> calculateDigest(final IRI identifier, final MessageDigest algorithm) {
        return supplyAsync(() -> computeDigest(identifier, algorithm));
    }

    @Override
    public Set<String> supportedAlgorithms() {
        return algorithms;
    }

    @Override
    public String generateIdentifier() {
        return idSupplier.get();
    }

    private File getFileFromIdentifier(final IRI identifier) {
        requireNonNull(identifier, "Identifier may not be null!");
        return of(identifier).map(IRI::getIRIString).filter(x -> x.startsWith("file:")).map(URI::create)
                .map(URI::getSchemeSpecificPart).map(x -> trimStart(x, "/")).map(x -> new File(basePath, x))
                .orElseThrow(
                        () -> new IllegalArgumentException("Could not create File object from IRI: " + identifier));
    }

    private MessageDigest computeDigest(final IRI identifier, final MessageDigest algorithm) {
        try (final InputStream input = new FileInputStream(getFileFromIdentifier(identifier))) {
            return updateDigest(algorithm, input);
        } catch (final IOException ex) {
            throw new UncheckedIOException("Error computing digest", ex);
        }
    }

    private static String trimStart(final String str, final String trim) {
        if (str.startsWith(trim)) {
            return trimStart(str.substring(trim.length()), trim);
        }
        return str;
    }

}