All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.foreach.common.filemanager.services.AbstractExpiringFileRepository Maven / Gradle / Ivy

/*
 * Copyright 2014 the original author or authors
 *
 * 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 com.foreach.common.filemanager.services;

import com.foreach.common.filemanager.business.*;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.PreDestroy;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Base class for a file repository which tracks its file resources and expires them
 * according to a specific {@link #expirationStrategy}. The actual meaning of expiration
 * depends on the particular implementation.
 * 

* A target repository is configured which delivers the original file resource that should * be adapted into an {@link ExpiringFileResource}. The expiring repository takes the identity * of the target repository, developers should only register the expiring repository. *

* This repository keeps an LRU type map of known file descriptors. The maximum number of * resources to track can be configured. Once this maximum is exceeded, the oldest file * resource will be evicted; after which point it will be fetched again from the target * repository. If a resource expires when evicted depends on the {@link #expireOnEvict} value. *

* In the base implementation folder resources do not expire but only the file resources * they return are converted to expiring resources. The actual folder actions are executed * directly on the target folder resource. This means that folder executions (for example listing * of files) can quickly cause evictions if {@link #maxItemsToTrack} is too low as every file * returned will be wrapped as an expiring file resource. * * @author Arne Vandamme * @see ExpiringFileRepository * @see CachingFileRepository * @since 1.4.0 */ @Slf4j @SuppressWarnings("WeakerAccess") public abstract class AbstractExpiringFileRepository extends AbstractFileRepository { /** * The target repository this implementation wraps. */ @Getter private final FileRepository targetFileRepository; /** * True if resources should be expired when the file repository is destroyed. */ @Getter private final boolean expireOnShutdown; /** * True if resources should be expired when they are evicted. */ @Getter private final boolean expireOnEvict; /** * Maximum number of file resources to track. Past this number the least recently fetched file descriptor * will be evicted. Depending on {@link #expireOnEvict} this means the actual resource will be expired immediately. */ @Getter private final int maxItemsToTrack; /** * Function which returns {@code true} if a file resource should be expired. */ @Getter private final Function expirationStrategy; private final Map trackedResources = Collections.synchronizedMap( new FileResourceCache() ); protected AbstractExpiringFileRepository( @NonNull FileRepository targetFileRepository, boolean expireOnShutdown, boolean expireOnEvict, int maxItemsToTrack, @NonNull Function expirationStrategy ) { super( targetFileRepository.getRepositoryId() ); this.targetFileRepository = targetFileRepository; this.expireOnShutdown = expireOnShutdown; this.expireOnEvict = expireOnEvict; this.maxItemsToTrack = maxItemsToTrack; this.expirationStrategy = expirationStrategy; } @Override public String getRepositoryId() { return targetFileRepository.getRepositoryId(); } @Override protected PathGenerator getPathGenerator() { return targetFileRepository instanceof AbstractFileRepository ? ( (AbstractFileRepository) targetFileRepository ).getPathGenerator() : null; } @Override public void setPathGenerator( PathGenerator pathGenerator ) { if ( targetFileRepository instanceof AbstractFileRepository ) { ( (AbstractFileRepository) targetFileRepository ).setPathGenerator( pathGenerator ); } else { throw new UnsupportedOperationException( "Target file repository does not implement AbstractFileRepository: path generator is not supported" ); } } @Override public FileDescriptor generateFileDescriptor() { return targetFileRepository.generateFileDescriptor(); } /** * Runs all tracked file resources through the expiration strategy, and expires where necessary. **/ @SuppressWarnings("WeakerAccess") public void expireTrackedItems() { try { LOG.trace( "Running file resource expiration for repository {}", getRepositoryId() ); new ArrayList<>( trackedResources.keySet() ).forEach( fd -> { T fileResource = trackedResources.get( fd ); if ( fileResource != null && expirationStrategy.apply( fileResource ) ) { stopTracking( fd ); expire( fileResource ); } } ); } catch ( Exception e ) { LOG.error( "Exception running file repository expiration", e ); } } @Override protected void validateFileDescriptor( FileDescriptor descriptor ) { // expiring file repository performs no descriptor validation } @Override protected void validateFolderDescriptor( FolderDescriptor descriptor ) { // expiring file repository performs no descriptor validation } @SuppressWarnings("unchecked") @Override public T createFileResource( boolean allocateImmediately ) { return (T) super.createFileResource( allocateImmediately ); } @SuppressWarnings("unchecked") @Override public T getFileResource( FileDescriptor descriptor ) { return (T) super.getFileResource( descriptor ); } @SuppressWarnings("unchecked") @Override public T createFileResource( File originalFile, boolean deleteOriginal ) throws IOException { return (T) super.createFileResource( originalFile, deleteOriginal ); } @SuppressWarnings("unchecked") @Override public T createFileResource( InputStream inputStream ) throws IOException { return (T) super.createFileResource( inputStream ); } @SuppressWarnings("unchecked") @Override public T createFileResource() { return (T) super.createFileResource(); } /** * Shutdown the repository. Depending on the value of {@link #expireOnShutdown} the tracked resources will be expired. */ @PreDestroy @Override public void shutdown() { trackedResources.values() .forEach( fileResource -> { if ( expirationStrategy.apply( fileResource ) || expireOnShutdown ) { expire( fileResource ); } else { evicted( fileResource, false ); } } ); trackedResources.clear(); } @Override protected final T buildFileResource( FileDescriptor descriptor ) { return trackedResources.computeIfAbsent( descriptor, fd -> { FileResource targetFileResource = targetFileRepository.getFileResource( fd ); return createExpiringFileResource( fd, targetFileResource ); } ); } @Override protected final FolderResource buildFolderResource( FolderDescriptor descriptor ) { FolderResource targetFolderResource = targetFileRepository.getFolderResource( descriptor ); return createExpiringFolderResource( targetFolderResource ); } @Override public FolderResource getRootFolderResource() { FolderResource rootFolderResource = targetFileRepository.getRootFolderResource(); return createExpiringFolderResource( rootFolderResource ); } protected abstract T createExpiringFileResource( FileDescriptor descriptor, FileResource targetFileResource ); protected FolderResource createExpiringFolderResource( FolderResource targetFolderResource ) { return new ExpiringFolderResource( targetFolderResource ); } /** * Eviction notice of a file resource. The second parameter indicates if the resource has expired in the process or not. * * @param fileResource that has been evicted * @param expired true if the resource has also been expired */ protected void evicted( T fileResource, boolean expired ) { } /** * Perform the actual expiration of a resource. * * @param fileResource to expire */ protected abstract void expire( T fileResource ); /** * Remove this descriptor from the tracked resources. This does not expire the matching resource. * * @param descriptor to stop tracking */ protected void stopTracking( FileDescriptor descriptor ) { trackedResources.remove( descriptor ); } /** * Run the expiration (removal of stale items) on all {@code AbstractExpiringFileRepository} implementations. * Detects all implementations of {@link AbstractExpiringFileRepository} in the {@link FileRepositoryRegistry} * and executes {@link AbstractExpiringFileRepository#expireTrackedItems()}. * * @param fileRepositoryRegistry that holds the repositories * @see FileRepositoryRegistry */ public static void expireTrackedItems( @NonNull FileRepositoryRegistry fileRepositoryRegistry ) { LOG.trace( "Tracked file resource expiration triggered" ); fileRepositoryRegistry.listRepositories() .stream() .map( r -> r instanceof FileRepositoryDelegate ? ( (FileRepositoryDelegate) r ).getActualImplementation() : r ) .filter( AbstractExpiringFileRepository.class::isInstance ) .map( AbstractExpiringFileRepository.class::cast ) .forEach( AbstractExpiringFileRepository::expireTrackedItems ); } private class FileResourceCache extends LinkedHashMap { FileResourceCache() { super( maxItemsToTrack + 1, .75F, true ); } @Override protected boolean removeEldestEntry( Map.Entry eldest ) { boolean shouldEvict = size() > maxItemsToTrack; if ( shouldEvict ) { if ( expireOnEvict || expirationStrategy.apply( eldest.getValue() ) ) { expire( eldest.getValue() ); evicted( eldest.getValue(), true ); } else { evicted( eldest.getValue(), false ); } } return shouldEvict; } } /** * Wrapper that ensures the results from the target folder resource are converted * to expiring file resources. */ @RequiredArgsConstructor private class ExpiringFolderResource implements FolderResource { private final FolderResource target; @Override public FolderDescriptor getDescriptor() { return target.getDescriptor(); } @Override public Optional getParentFolderResource() { return target.getParentFolderResource().map( AbstractExpiringFileRepository.this::createExpiringFolderResource ); } @Override public FileRepositoryResource getResource( String relativePath ) { return wrap( target.getResource( relativePath ) ); } @Override public Collection findResources( String pattern ) { return wrap( target.findResources( pattern ) ); } @Override public boolean delete( boolean deleteChildren ) { if ( deleteChildren ) { boolean deleted = deleteChildren(); return deleted || target.delete( false ); } return target.delete( false ); } @Override public boolean deleteChildren() { Collection resources = target.findResources( "*" ); if ( !resources.isEmpty() ) { try { resources.forEach( r -> { if ( r instanceof FileResource ) { FileResource fileResource = (FileResource) r; T wrapped = trackedResources.get( fileResource.getDescriptor() ); if ( wrapped != null ) { wrapped.delete(); } else { fileResource.delete(); } } else { AbstractExpiringFileRepository.this.createExpiringFolderResource( (FolderResource) r ).delete( true ); } } ); } catch ( Exception ignore ) { return false; } return true; } return false; } @Override public boolean create() { return target.create(); } @Override public boolean exists() { return target.exists(); } @Override public String getFolderName() { return target.getFolderName(); } @Override public FolderResource getFolderResource( String relativePath ) { return AbstractExpiringFileRepository.this.createExpiringFolderResource( target.getFolderResource( relativePath ) ); } @Override public FileResource getFileResource( String relativePath ) { return createExpiringFileResource( target.getFileResource( relativePath ) ); } @Override public FileResource createFileResource() { return createExpiringFileResource( target.createFileResource() ); } @Override public Collection listFiles() { return wrap( target.listFiles() ); } @Override public Collection listFolders() { return wrap( target.listFolders() ); } @Override public Collection listResources( boolean recurseFolders, Class resourceType ) { return wrap( target.listResources( recurseFolders, resourceType ) ); } @Override public Collection listResources( boolean recurseFolders ) { return wrap( target.listResources( recurseFolders ) ); } @Override public Collection findResources( String pattern, Class resourceType ) { return wrap( target.findResources( pattern, resourceType ) ); } @Override public boolean isEmpty() { return target.isEmpty(); } @Override public URI getURI() { return target.getURI(); } @Override public boolean equals( Object obj ) { return obj == this || ( obj instanceof FolderResource && target.equals( obj ) ); } @Override public int hashCode() { return target.hashCode(); } private T createExpiringFileResource( FileResource targetFileResource ) { return AbstractExpiringFileRepository.this.createExpiringFileResource( targetFileResource.getDescriptor(), targetFileResource ); } private Collection wrap( Collection original ) { return original.stream() .map( this::wrap ) .collect( Collectors.toList() ); } @SuppressWarnings("unchecked") private U wrap( U resource ) { if ( resource instanceof FolderResource ) { return (U) AbstractExpiringFileRepository.this.createExpiringFolderResource( (FolderResource) resource ); } return (U) createExpiringFileResource( (FileResource) resource ); } } /** * Configures a time-based expiration function which expires resources * if they have not been accessed for longer than {@code maxUnusedDuration} or if their * creation time was longer than {@code maxAge} ago. Whichever condition matches. *

* Both time durations are in milliseconds. The reference for access time is {@link ExpiringFileResource#getLastAccessTime()}, * the reference for age is {@link ExpiringFileResource#getCreationTime()}. *

* Specifying zero or less will skip that condition. * * @param maxUnusedDuration maximum number of milliseconds that is allowed since last access time * @param maxAge maximum number of milliseconds that is allowed since cache has been created * @return expiration function */ public static Function timeBasedExpirationStrategy( long maxUnusedDuration, long maxAge ) { return fr -> { long now = System.currentTimeMillis(); if ( maxUnusedDuration > 0 && ( now - fr.getLastAccessTime() ) > maxUnusedDuration ) { return true; } if ( maxAge > 0 ) { long age = now - fr.getCreationTime(); return age != now && age > maxAge; } return false; }; } }