software.coolstuff.springframework.owncloud.service.impl.rest.OwncloudRestResourceServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for software.coolstuff.springframework.owncloud.service.impl.rest.OwncloudRestResourceServiceImpl.java

Source

/*-
 * #%L
 * owncloud-spring-boot-starter
 * %%
 * Copyright (C) 2016 - 2017 by the original Authors
 * %%
 * 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 3 of the
 * License, or (at your option) any later version.
 * 
 * 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.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package software.coolstuff.springframework.owncloud.service.impl.rest;

import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.impl.SardineException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.HttpStatus;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import software.coolstuff.springframework.owncloud.exception.resource.*;
import software.coolstuff.springframework.owncloud.model.OwncloudFileResource;
import software.coolstuff.springframework.owncloud.model.OwncloudQuota;
import software.coolstuff.springframework.owncloud.model.OwncloudResource;
import software.coolstuff.springframework.owncloud.service.api.OwncloudResourceService;
import software.coolstuff.springframework.owncloud.service.impl.OwncloudUtils;
import software.coolstuff.springframework.owncloud.service.impl.rest.OwncloudRestProperties.ResourceServiceProperties.CacheProperties;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;

@Slf4j
public class OwncloudRestResourceServiceImpl
        implements OwncloudResourceService, OwncloudRestService, OwncloudResolveRootUriService {

    private static final String URI_SUFFIX = "/remote.php/dav/files/{username}/";
    private static final String SLASH = "/";
    private static final String QUOTE = "\"";

    private final RestTemplate restTemplate;
    private final OwncloudRestProperties properties;
    private final SardineCacheLoader sardineCacheLoader;
    private final OwncloudRestUserServiceExtension userService;
    private final String rootUri;

    private LoadingCache<String, Sardine> sardineCache;

    public OwncloudRestResourceServiceImpl(final RestTemplateBuilder builder,
            final OwncloudRestProperties properties, final SardineCacheLoader sardineCacheLoader,
            final OwncloudRestUserServiceExtension userService) throws MalformedURLException {
        this.properties = properties;
        this.sardineCacheLoader = sardineCacheLoader;
        this.userService = userService;

        URL locationURL = OwncloudRestUtils.checkAndConvertLocation(properties.getLocation());
        this.rootUri = appendOptionalSuffix(locationURL, URI_SUFFIX);
        log.debug("Build the RestTemplate based on Root URI {}", rootUri);
        restTemplate = builder.requestFactory(HttpComponentsClientHttpRequestFactory.class)
                .messageConverters(new ByteArrayHttpMessageConverter()).rootUri(rootUri).build();
    }

    protected String appendOptionalSuffix(URL url, String suffix) {
        if (StringUtils.isBlank(suffix)) {
            return url.toString();
        }
        return StringUtils.stripEnd(url.toString(), SLASH) + SLASH + StringUtils.stripStart(suffix, SLASH);
    }

    @Override
    public RestTemplate getRestTemplate() {
        return restTemplate;
    }

    @PostConstruct
    public void afterPropertiesSet() throws Exception {
        log.debug("Build the Sardine Cache");
        this.sardineCache = buildSardineCache();
    }

    protected LoadingCache<String, Sardine> buildSardineCache() {
        CacheProperties cacheProperties = properties.getResourceService().getSardineCache();
        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
        if (cacheProperties.getConcurrencyLevel() != null) {
            builder.concurrencyLevel(cacheProperties.getConcurrencyLevel());
        }
        if (cacheProperties.getExpireAfterAccess() != null
                && cacheProperties.getExpireAfterAccessTimeUnit() != null) {
            builder.expireAfterAccess(cacheProperties.getExpireAfterAccess(),
                    cacheProperties.getExpireAfterAccessTimeUnit());
        }
        if (cacheProperties.getExpireAfterWrite() != null
                && cacheProperties.getExpireAfterWriteTimeUnit() != null) {
            builder.expireAfterWrite(cacheProperties.getExpireAfterWrite(),
                    cacheProperties.getExpireAfterWriteTimeUnit());
        }
        if (cacheProperties.getInitialCapacity() != null) {
            builder.initialCapacity(cacheProperties.getInitialCapacity());
        }
        if (cacheProperties.getMaximumSize() != null) {
            builder.maximumSize(cacheProperties.getMaximumSize());
        }
        if (cacheProperties.getMaximumWeight() != null) {
            builder.maximumWeight(cacheProperties.getMaximumWeight());
        }
        if (cacheProperties.getRefreshAfterWrite() != null
                && cacheProperties.getRefreshAfterWriteTimeUnit() != null) {
            builder.refreshAfterWrite(cacheProperties.getRefreshAfterWrite(),
                    cacheProperties.getRefreshAfterWriteTimeUnit());
        }
        return builder.build(sardineCacheLoader);
    }

    @Override
    public List<OwncloudResource> list(URI relativeTo) {
        URI searchPath = resolveAsDirectoryURI(relativeTo);
        try {
            return listAllOwncloudResourcesOf(searchPath);
        } catch (SardineException e) {
            SardineExceptionHandlerEnvironment handlerEnvironment = SardineExceptionHandlerEnvironment.builder()
                    .uri(URI.create(searchPath.toString())).sardineException(e).build();
            return handleSardineException(handlerEnvironment).map(Lists::newArrayList).orElse(new ArrayList<>());
        } catch (IOException e) {
            throw new OwncloudRestResourceException(e);
        }
    }

    private URI resolveAsDirectoryURI(URI relativeTo) {
        URI userRoot = getUserRoot();
        if (relativeTo == null || StringUtils.isBlank(relativeTo.getPath())) {
            return userRoot;
        }
        return URI
                .create(UriComponentsBuilder.fromUri(userRoot).path(relativeTo.getPath()).path(SLASH).toUriString())
                .normalize();
    }

    private URI getUserRoot() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return getResolvedRootUri(authentication.getName());
    }

    @Override
    public URI getResolvedRootUri(String username) {
        log.debug("Get base URI of User {} (under Root URI {})", username, rootUri);
        return URI.create(StringUtils.replace(rootUri, "{username}", username));
    }

    private List<OwncloudResource> listAllOwncloudResourcesOf(URI searchPath) throws IOException {
        List<OwncloudResource> owncloudResources = new ArrayList<>();
        listOwncloudResourcesOf(searchPath)
                .peek(owncloudResource -> log.debug("Add Owncloud Resource {}", owncloudResource))
                .forEach(owncloudResources::add);
        if (isAddParentResourceToCollection(searchPath, owncloudResources)) {
            listParentOwncloudResourcesOf(searchPath)
                    .peek(owncloudResource -> log.debug("Add Owncloud Resource {}", owncloudResource))
                    .forEach(owncloudResources::add);
        }
        return owncloudResources;
    }

    private Stream<OwncloudResource> listOwncloudResourcesOf(URI searchPath) throws IOException {
        Sardine sardine = getSardine();
        URI userRoot = getUserRoot();
        log.debug("Get the List of WebDAV Resources based by URI {}", searchPath);
        List<DavResource> davResources = sardine.list(searchPath.toString());
        val searchPathConversionProperties = OwncloudResourceConversionProperties.builder().rootPath(userRoot)
                .searchPath(searchPath).renamedSearchPath(".").build();
        return davResources.stream()
                .map(davResource -> createOwncloudResourceFrom(davResource, searchPathConversionProperties))
                .map(modifyingResource -> renameOwncloudResource(modifyingResource,
                        searchPathConversionProperties));
    }

    protected Sardine getSardine() throws OwncloudSardineCacheException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        try {
            log.debug("Get the Sardine Implementation of User {}", username);
            return sardineCache.get(username);
        } catch (Exception e) {
            val logMessage = String.format(
                    "Cannot get the Sardine Implementation based by User %s from the Sardine Cache", username);
            log.error(logMessage, e);
            throw new OwncloudSardineCacheException(logMessage, e);
        }
    }

    @Data
    @Builder
    private static class OwncloudResourceConversionProperties {
        private final URI rootPath;
        private URI searchPath;
        private String renamedSearchPath;
    }

    private OwncloudRestResourceExtension createOwncloudResourceFrom(DavResource davResource,
            OwncloudResourceConversionProperties conversionProperties) {
        log.debug("Create OwncloudResource based on DavResource {}", davResource.getHref());
        MediaType mediaType = MediaType.valueOf(davResource.getContentType());
        URI rootPath = conversionProperties.getRootPath();
        URI href = rootPath.resolve(davResource.getHref());
        String name = davResource.getName();
        if (davResource.isDirectory() && href.equals(rootPath)) {
            name = SLASH;
        }
        LocalDateTime lastModifiedAt = LocalDateTime.ofInstant(davResource.getModified().toInstant(),
                ZoneId.systemDefault());
        href = rootPath.relativize(href);
        href = URI.create(SLASH).resolve(href).normalize(); // prepend "/" to the href
        OwncloudRestResourceExtension owncloudResource = OwncloudRestResourceImpl.builder().href(href).name(name)
                .lastModifiedAt(lastModifiedAt).mediaType(mediaType)
                .eTag(StringUtils.strip(davResource.getEtag(), QUOTE)).build();
        if (davResource.isDirectory()) {
            return owncloudResource;
        }
        return OwncloudRestFileResourceImpl.fileBuilder().owncloudResource(owncloudResource)
                .contentLength(davResource.getContentLength()).build();
    }

    private OwncloudRestResourceExtension renameOwncloudResource(OwncloudRestResourceExtension resource,
            OwncloudResourceConversionProperties conversionProperties) {
        if (StringUtils.isBlank(conversionProperties.getRenamedSearchPath())) {
            return resource;
        }
        URI resourcePath = URI.create(UriComponentsBuilder.fromUri(conversionProperties.getRootPath())
                .path(resource.getHref().getPath()).toUriString()).normalize();
        if (conversionProperties.getSearchPath().equals(resourcePath)) {
            log.debug("Rename OwncloudResource {} based by {} to {}", resource.getName(), resource.getHref(),
                    conversionProperties.getRenamedSearchPath());
            resource.setName(conversionProperties.getRenamedSearchPath());
        }
        return resource;
    }

    private boolean isAddParentResourceToCollection(URI searchPath, List<OwncloudResource> owncloudResources) {
        return properties.getResourceService().isAddRelativeDownPath() && isNotResolvedToRootURI(searchPath)
                && containsNotOnlyOneFileResource(owncloudResources);
    }

    private boolean isNotResolvedToRootURI(URI path) {
        return !isResolvedToRootURI(path);
    }

    private boolean isResolvedToRootURI(URI path) {
        URI userRoot = getUserRoot();
        if (path.isAbsolute()) {
            return userRoot.equals(path);
        }
        return userRoot.equals(resolveAsDirectoryURI(path));
    }

    private boolean containsNotOnlyOneFileResource(List<OwncloudResource> owncloudResources) {
        return !containsOnlyOneFileResource(owncloudResources);
    }

    private boolean containsOnlyOneFileResource(List<OwncloudResource> owncloudResources) {
        return owncloudResources.size() == 1 && !OwncloudUtils.isDirectory(owncloudResources.get(0));
    }

    private Stream<OwncloudResource> listParentOwncloudResourcesOf(URI searchPath) throws IOException {
        URI parentPath = URI.create(UriComponentsBuilder.fromUri(searchPath.normalize()).path("/../").toUriString())
                .normalize();
        log.debug("Get the List of WebDAV Resources based by Parent URI {}", parentPath);
        URI userRoot = getUserRoot();
        val parentDirectoryConversionProperties = OwncloudResourceConversionProperties.builder().rootPath(userRoot)
                .searchPath(parentPath).renamedSearchPath("..").build();
        Sardine sardine = getSardine();
        List<DavResource> davResources = sardine.list(parentPath.toString(), 0);
        return davResources.stream()
                .map(davResource -> createOwncloudResourceFrom(davResource, parentDirectoryConversionProperties))
                .map(modifyingResource -> renameOwncloudResource(modifyingResource,
                        parentDirectoryConversionProperties));
    }

    private static class SardineExceptionHandlerEnvironment {
        @Getter
        private final URI uri;
        @Getter
        private final SardineException sardineException;

        private final Map<Integer, Function<SardineExceptionHandlerEnvironment, Optional<OwncloudResource>>> statusCodeHandler = new HashMap<>();

        @Builder
        private SardineExceptionHandlerEnvironment(final URI uri, final SardineException sardineException) {
            Validate.notNull(uri);
            Validate.notNull(sardineException);
            this.uri = uri;
            this.sardineException = sardineException;
            registerHandler(HttpStatus.SC_NOT_FOUND, this::handleStatusCodeNotFound);
        }

        void registerHandler(int statusCode,
                Function<SardineExceptionHandlerEnvironment, Optional<OwncloudResource>> statusCodeHandler) {
            this.statusCodeHandler.put(statusCode, statusCodeHandler);
        }

        private Optional<OwncloudResource> handleStatusCodeNotFound(
                SardineExceptionHandlerEnvironment environment) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            throw new OwncloudResourceNotFoundException(environment.getUri(), authentication.getName());
        }

        Function<SardineExceptionHandlerEnvironment, Optional<OwncloudResource>> getHandlerFor(int statusCode) {
            return statusCodeHandler.get(statusCode);
        }
    }

    private Optional<OwncloudResource> handleSardineException(SardineExceptionHandlerEnvironment environment) {
        SardineException sardineException = environment.getSardineException();
        int statusCode = sardineException.getStatusCode();
        Function<SardineExceptionHandlerEnvironment, Optional<OwncloudResource>> statusCodeHandler = environment
                .getHandlerFor(statusCode);
        if (statusCodeHandler != null) {
            return statusCodeHandler.apply(environment);
        }
        log.error("Unmapped HTTP-Status {}. Reason-Phrase: {}", statusCode, sardineException.getResponsePhrase());
        throw new OwncloudRestResourceException("Unmapped returned HTTP-Status " + statusCode, sardineException);
    }

    @Override
    public Optional<OwncloudResource> find(URI path) {
        URI searchPath = resolveAsDirectoryURI(path);
        try {
            return findOwncloudResourceOn(searchPath);
        } catch (SardineException e) {
            SardineExceptionHandlerEnvironment handlerEnvironment = SardineExceptionHandlerEnvironment.builder()
                    .uri(URI.create(searchPath.toString())).sardineException(e).build();
            handlerEnvironment.registerHandler(HttpStatus.SC_NOT_FOUND, environment -> Optional.empty());
            return handleSardineException(handlerEnvironment);
        } catch (IOException e) {
            throw new OwncloudRestResourceException(e);
        }
    }

    private Optional<OwncloudResource> findOwncloudResourceOn(URI searchPath) throws IOException {
        List<DavResource> davResources = getSardine().list(searchPath.toString(), 0);
        val conversionProperties = OwncloudResourceConversionProperties.builder().rootPath(getUserRoot())
                .searchPath(searchPath).build();
        return Optional.ofNullable(davResources.stream().findFirst()
                .map(davResource -> createOwncloudResourceFrom(davResource, conversionProperties)).orElse(null));
    }

    @Override
    public OwncloudResource createDirectory(URI directory) {
        Optional<OwncloudResource> existingDirectory = find(directory);
        if (existingDirectory.isPresent()) {
            return existingDirectory.filter(OwncloudUtils::isDirectory)
                    .orElseThrow(() -> new OwncloudNoDirectoryResourceException(directory));
        }
        return createNonExistingDirectory(directory);
    }

    private OwncloudResource createNonExistingDirectory(URI directory) {
        URI directoryURI = resolveAsDirectoryURI(directory);
        try {
            getSardine().createDirectory(directoryURI.toString());
            return find(directory).orElse(null);
        } catch (SardineException e) {
            SardineExceptionHandlerEnvironment handlerEnvironment = SardineExceptionHandlerEnvironment.builder()
                    .uri(URI.create(directory.toString())).sardineException(e).build();
            return handleSardineException(handlerEnvironment).get();
        } catch (IOException e) {
            throw new OwncloudRestResourceException(e);
        }
    }

    @Override
    public void delete(OwncloudResource resource) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        URI resolvedUri = resolveAsFileURI(resource.getHref(), authentication.getName());
        try {
            restTemplate.execute(resolvedUri, HttpMethod.DELETE,
                    clientHttpRequest -> createRestCallback(clientHttpRequest, authentication), null);
        } catch (RestClientException restClientException) {
            RestClientExceptionHandlerEnvironment exceptionHandlerEnvironment = RestClientExceptionHandlerEnvironment
                    .builder().restClientException(restClientException).requestURI(resource.getHref())
                    .username(authentication.getName()).build();
            OwncloudRestUtils.handleRestClientException(exceptionHandlerEnvironment);
        }
    }

    private URI resolveAsFileURI(URI relativeTo, String username) {
        URI userRoot = getResolvedRootUri(username);
        if (relativeTo == null || StringUtils.isBlank(relativeTo.getPath())) {
            return userRoot;
        }
        return URI.create(UriComponentsBuilder.fromUri(userRoot).path(relativeTo.getPath()).toUriString())
                .normalize();
    }

    private void createRestCallback(ClientHttpRequest clientHttpRequest, Authentication authentication)
            throws IOException {
        OwncloudRestUtils.addAuthorizationHeader(clientHttpRequest.getHeaders(), authentication);
    }

    @Override
    public InputStream getInputStream(OwncloudFileResource resource) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        PipedInputStreamRestSynchronizer pipedInputStreamSynchronizer = PipedInputStreamRestSynchronizer.build()
                .authentication(authentication).owncloudRestProperties(properties).restOperations(restTemplate)
                .uri(resource.getHref()).uriResolver(this::resolveAsFileURI).build();
        return pipedInputStreamSynchronizer.getInputStream();
    }

    @Override
    public OutputStream getOutputStream(OwncloudFileResource resource) {
        checkOwncloudFileResource(resource);
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        PipedOutputStreamRestSynchronizer pipedOutputStreamSynchronizer = PipedOutputStreamRestSynchronizer
                .builder().authentication(authentication).mediaType(resource.getMediaType())
                .owncloudRestProperties(properties).restOperations(restTemplate).uri(resource.getHref())
                .uriResolver(this::resolveAsFileURI).build();
        return pipedOutputStreamSynchronizer.getOutputStream();
    }

    private void checkOwncloudFileResource(OwncloudFileResource resource) {
        Validate.notNull(resource);
        Validate.notNull(resource.getHref());
        Validate.notNull(resource.getMediaType());
        if (OwncloudUtils.isDirectory(resource)) {
            throw new OwncloudNoFileResourceException(resource.getHref());
        }
    }

    @Override
    public OutputStream getOutputStream(URI path, MediaType mediaType) {
        Optional<OwncloudResource> optionalExistingFile = find(path);
        if (optionalExistingFile.isPresent()) {
            OwncloudFileResource existingFile = optionalExistingFile.filter(OwncloudUtils::isNotDirectory)
                    .map(OwncloudUtils::toOwncloudFileResource)
                    .orElseThrow(() -> new OwncloudNoFileResourceException(path));
            return getOutputStream(existingFile);
        }
        OwncloudFileResource resource = OwncloudRestFileResourceImpl.fileBuilder()
                .owncloudResource(OwncloudRestResourceImpl.builder().href(path).mediaType(mediaType).build())
                .build();
        return getOutputStream(resource);
    }

    @Override
    public OwncloudQuota getQuota() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return userService.getQuota(authentication.getName());
    }
}