package de.axone.web; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.axone.async.ThreadQueue; import de.axone.gfx.ImageScaler; import de.axone.thread.ThreadsafeContractor; class PictureBuilderBuilderImpl implements PictureBuilderBuilder { public static final Logger log = LoggerFactory.getLogger( PictureBuilderNG.class ); private static final String MAIN = "main", PLAIN = "plain" ; private final Path homedir, cachedir, maindir ; private final int hashLength; private Optional<Path> watermark = Optional.empty(); private static ThreadQueue threadQueue = new ThreadQueue( 4 ); /** * @param homedir something like '/home/shop/pictures' * @param maindir absolute or relative directory where uploaded files are stored. * In case of <code>null</code> <code>homedir/main</code> is used * @param cachedir absolute or relative directory for created files * In case of <code>null</code> <code>homedir</code> is used * @param hashLength length of the directories for storing a hashed version of the identifier */ PictureBuilderBuilderImpl( Path homedir, Path maindir, Path cachedir, int hashLength ) { this.homedir = homedir; if( maindir == null ) { this.maindir = homedir.resolve( MAIN ); } else if( maindir.isAbsolute() ) { this.maindir = maindir; } else if( maindir.equals( homedir ) ) { this.maindir = maindir; } else { this.maindir = homedir.resolve( maindir ); } if( cachedir == null ) { this.cachedir = homedir; } else if( cachedir.isAbsolute() ) { this.cachedir = cachedir; } else { this.cachedir = homedir.resolve( cachedir ); } this.hashLength = hashLength; } // copy constructor private PictureBuilderBuilderImpl( PictureBuilderBuilderImpl other ) { this.homedir = other.homedir; this.maindir = other.maindir; this.cachedir = other.cachedir; this.hashLength = other.hashLength; } @Override public PictureBuilderBuilder watermark( String watermark ) { PictureBuilderBuilderImpl result = new PictureBuilderBuilderImpl( this ); result.watermark = watermark != null ? Optional.of( Paths.get( watermark ) ) : Optional.empty(); return result; } @Override public PictureBuilderNG builder( String identifier, int index ) { return new PictureBuilderNGImpl( identifier, index ); } private class PictureBuilderNGImpl implements PictureBuilderNG { private final String identifier; private final int index; private final String hashName; public PictureBuilderNGImpl( String identifier, int index ) { this.identifier = identifier; this.index = index; this.hashName = hashLength > 0 ? hashName( identifier, hashLength ) : null; } @Override public int fileCount() { Path dir = mainDir(); if( ! Files.isDirectory( dir ) ) return 0; int result; try( Stream<Path> stream = Files.list( dir ) ) { result = (int) stream .filter( JPEG ) .count(); } catch( IOException e ) { throw new Error( "Error reading: " + dir ); } return result; } @Override public boolean exists() { Optional<Path> mainFile = mainFile(); return mainFile.isPresent() && Files.isReadable( mainFile.get() ); } @Override public Optional<Path> get( int size ) throws IOException { return get( size, watermarkFile(), true, false ); } private Path mainDir() { return pathTo( maindir, hashName, identifier ); } private Optional<Path> watermarkFile() { if( ! watermark.isPresent() ) return Optional.empty(); return Optional.of( homedir .resolve( watermark.get() ) ); } private Optional<Path> cachedFile( int size, Optional<Path> watermark ) throws IOException { Path fileCacheDir = createCacheDir( cachedir, identifier, size, hashName, watermark ); Optional<Path> mainResult = mainFile(); if( !mainResult.isPresent() ) return Optional.empty(); Path main = mainResult.get(); Path mainPath = fileCacheDir.resolve( main.getFileName().toString() + '_' + Files.getLastModifiedTime( main ).toMillis() + ".jpeg" ); return Optional.of( mainPath ); } private Optional<Path> get( int size, Optional<Path> watermark, boolean doPrescale, boolean hq ) throws IOException { if( ! exists() ) return Optional.empty(); Optional<Path> cachedFileResult = cachedFile( size, watermark ); if( ! cachedFileResult.isPresent() ) return Optional.empty(); Path cachedFile = cachedFileResult.get(); if( ! Files.isRegularFile( cachedFile ) ) { long start = System.currentTimeMillis(); Optional<Path> mainFileResult = mainFile(); if( !mainFileResult.isPresent() ) return Optional.empty(); Path mainFile = mainFileResult.get(); //synchronized( lock ) { ThreadQueue.Lock lock = null; try { // Get lock. Only if called in outer recursion if( ! hq ) lock = threadQueue.lock( identifier + "___" + index ); // Second try. Other thread could have created it by now // We do this because we don't want the lock to reach out to // the first "getCacheFile" call Optional<Path> secondTry = cachedFile( size, watermark ); if( ! secondTry.isPresent() ) return Optional.empty(); if( ! Files.isRegularFile( secondTry.get() ) ){ // Look for old version Path cacheDir = cachedFile.getParent(); List<Path> oldVersions; try( Stream<Path> stream = Files.list( cacheDir ) ) { oldVersions = stream .filter( new OldVersionOf( cachedFile ) ) .collect( Collectors.toList() ) ; } for( Path p : oldVersions ) { boolean ok = Files.deleteIfExists( p ); if( ! ok ) throw new IOException( "Cannot delete: " + p ); } // Get precached image if this is'nt a request for it Optional<Path> imageFile; if( doPrescale ) { imageFile = get( 1000, Optional.empty(), false, true ); } else { imageFile = Optional.of( mainFile ); } if( !imageFile.isPresent() ) return Optional.empty(); ImageScaler.instance().scale( cachedFile, imageFile.get(), watermark, size, hq ); } } finally { if( ! hq ) threadQueue.releaseLock( lock ); long dur = System.currentTimeMillis() - start; log.info( "{} Rendered in {} ms", cachedFile, dur ); } } return Optional.of( cachedFile ); } @Override public String toString() { return identifier + '/' + index; } private Optional<Path> mainFile; Optional<Path> mainFile() { if( mainFile == null ) { try { mainFile = findFile( maindir, hashName, identifier, index ); } catch( IOException e ) { mainFile = Optional.empty(); throw new Error( e ); } } return mainFile; } private class OldVersionOf implements Predicate<Path> { private String mainFileName; OldVersionOf( Path mainFile ) { mainFileName = mainFile.getFileName().toString(); } @Override public boolean test( Path path ) { return path.getFileName().toString().startsWith( mainFileName ); } } @Override public Path lookingAt() { return mainDir(); }; } private static ThreadsafeContractor cacheDirLocker8 = new ThreadsafeContractor(); private static Path createCacheDir( Path cachedir, String identifier, int size, String hashName, Optional<Path> watermark ) throws IOException { // Pictures without watermark got in PLAIN dir. Optional<String> hashedWatermark = hashWatermark( watermark ); String watername = hashedWatermark.isPresent() ? hashedWatermark.get() : PLAIN; // .../cache/shoppng Path dir = cachedir.resolve( watername ); // .../cache/shoppng/1/12345 Path sub = pathTo( dir, hashName, identifier ); // .../cache/shoppng/1/12345/1000 Path sub2 = sub.resolve( Integer.toString( size ) ); // Create chache dir if not exists // Only enter synchronized if needed cacheDirLocker8 .when( () -> { return ! Files.isDirectory( sub2 ); } ) .then( () -> { Files.createDirectories( sub2 ); } ) ; return sub2; } static Optional<Path> findFile( Path maindir, String hashName, String identifier, int index ) throws IOException { Path dir = pathTo( maindir, hashName, identifier ); if( ! dir.toFile().isDirectory() ) return Optional.empty(); if( index <= 0 ) { Path candidate = dir.resolve( identifier + ".jpg" ); File candidateFile = candidate.toFile(); if( candidateFile.isFile() ) { return Optional.of( candidate ); } } List<Path> files; try( Stream<Path> stream = Files.list( dir ) ) { files = stream .filter( JPEG ) .sorted() .collect( Collectors.toList() ) ; } if( files.size() <= index ) return Optional.empty(); return Optional.of( files.get( index ) ); } private static Path pathTo( Path dir, String hashName, String identifier ) { if( hashName != null ) { return dir.resolve( hashName ).resolve( identifier ); } else { return dir.resolve( identifier ); } } static String hashName( String name, int length ) { int nameLen = name.length(); if( length >= nameLen ) return name.toLowerCase(); // Find trailing '0' chars int iNon0; for( iNon0 = 0; iNon0 < nameLen-length; iNon0++ ) { if( name.charAt( iNon0 ) != '0' ) break; } return name.substring( iNon0, iNon0+length ).toLowerCase(); } static Optional<String> hashWatermark( Optional<Path> watermark ) { if( ! watermark.isPresent() ) return Optional.empty(); return Optional.of( watermark.get().getFileName().toString().replaceAll( "\\W", "" ).toLowerCase() ); } public static final Predicate<Path> JPEG = ( path ) -> { return path.toString().endsWith( ".jpg" ); }; }