package de.axone.web; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.axone.async.ThreadQueue; import de.axone.gfx.ImageScaler; /** * Creates a watermarked picture and caches it * * @author flo */ public abstract class PictureBuilder { public static final Logger log = LoggerFactory.getLogger( PictureBuilder.class ); //private static final String imageioLock = "ImageIO read lock"; private final String mainDir; private final File cacheDir; private final File main; private final String identifier; private final int index; private final int hashLength; public PictureBuilder(File cacheDir, String identifier) { this( cacheDir, identifier, 0 ); } public PictureBuilder(File cacheDir, String identifier, int index ) { this( cacheDir, "main", identifier, index, 1 ); } public PictureBuilder(File cacheDir, String mainDir, String identifier, int index, int hashLength ) { this.cacheDir = cacheDir; this.mainDir = mainDir; this.identifier = identifier; this.hashLength = hashLength; String main = findMain( cacheDir, identifier, index ); if( main != null ){ this.main = new File( cacheDir, main ); } else { this.main = null; } this.index = index; } /* * Find the main file * * (the 'main' file is that file which we are working with) */ private String findMain( File path, String name, int index ) { //E.rr( path.getAbsolutePath() ); //E.rr( name ); //E.rr( index ); StringBuilder builder = new StringBuilder(); String hashDir = hashName( name, hashLength ); //E.rr( mainDir ); builder.append( '/' ); if( mainDir != null ) builder.append( mainDir ).append( '/' ); //E.rr( hashDir ); if( hashDir != null ) builder.append( hashDir ).append( '/' ); //E.rr( name ); builder.append( name );; //E.rr( builder.toString() ); // Try to find main file without dir operation boolean found = false; if( index == 0 ) { StringBuilder probe = new StringBuilder( builder ); probe.append( '/' ).append( name ).append( ".jpg" ); //E.rr( probe ); File f = new File( path, probe.toString() ); //E.rr( f.getAbsolutePath() ); if( f.isFile() && f.canRead() ){ found = true; builder = probe; } } //E.rr( found ); if( !found ){ File mainDir = new File( path, builder.toString() ); //E.rr( mainDir.getAbsolutePath() ); File[] mainFilesA = mainDir.listFiles( JPEG ); if( mainFilesA != null ) { List<File> mainFiles = Arrays.asList( mainFilesA ); Collections.sort( mainFiles, new JpegSorter( name ) ); if( index >= mainFiles.size() ) { index = mainFiles.size() - 1; } if( index >= 0 ){ builder.append( '/' ).append( mainFiles.get( index ).getName() ); found = true; } } } //E.rr( found ); if( found ){ return builder.toString(); } else { return null; } } public static int fileCount( File path, String name, int hashLength ){ StringBuilder builder = new StringBuilder(); String hashDir = hashName( name, hashLength ); builder.append( "/main/" ) .append( hashDir ).append( '/' ) .append( name ); File mainDir = new File( path, builder.toString() ); File[] mainFiles = mainDir.listFiles( JPEG ); if( mainFiles != null ){ return mainFiles.length; } else { return 0; } } public boolean exists() { return main != null && main.isFile() && main.canRead(); } public File get( int size, File watermark ) throws IOException { return get( size, watermark, true, false ); } // Der Lock muss wohl static sein, damit auch von verschiedenen Klassen // kein synchroner zugriff möglich ist. //private static String lock = "I am a unique Lock"; private static ThreadQueue threadQueue = new ThreadQueue( 4 ); private File get( int size, File watermark, boolean doPrescale, boolean hq ) throws IOException { if( ! exists() ) return null; File cache = getCacheFile( size, watermark ); //E.rr( cache.getAbsolutePath() ); if( !cache.exists() ) { long start = System.currentTimeMillis(); //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 if( ! getCacheFile( size, watermark ).exists() ){ // Look for old version File dir = cache.getParentFile(); File [] fileList = dir.listFiles( new OldFilenameFilter() ); if( fileList != null ) for( File oldFile : fileList ) { boolean ok = oldFile.delete(); if( ! ok ) throw new IOException( "Cannot delete: " + oldFile.getAbsolutePath() ); } // Get precached image if this is'nt a request for it File imageFile; if( doPrescale ) { imageFile = get( 1000, null, false, true ); } else { imageFile = main; } ImageScaler.instance().scale( cache, imageFile, watermark, size, hq ); } } finally { if( ! hq ) threadQueue.releaseLock( lock ); long dur = System.currentTimeMillis() - start; log.info( "Rendered in {} ms", dur ); } } return cache; } /* * Calculate Size (x/y) using Outer-Box algorithm * * In words: Get the size so that the resulting image whould fit into */ /* private Dim mkOuterbox( int size, int originalWidth, int originalHeight ) { Dim res = new Dim(); double max = originalWidth > originalHeight ? originalWidth : originalHeight; res.w = (int) ( ( originalWidth / max ) * size ); res.h = (int) ( ( originalHeight / max ) * size ); return res; } */ private File getCacheFile( int size, File watermark ) throws IOException { File dir = getCacheDir( size, watermark ); long last = main.lastModified(); File file = new File( dir, main.getName() + "_" + last + ".jpeg" ); return file; } private File getCacheDir( int size, File watermark ) throws IOException { String waterDir; if( watermark != null ) { waterDir = hashWatermark( watermark ); } else { waterDir = "plain"; } // Create chache dir if not exists File subWatermark = new File( cacheDir, waterDir ); String hash = hashName( identifier, hashLength ); File subName; if( hash != null ){ File subHash = new File( subWatermark, hash ); subName = new File( subHash, identifier ); } else { subName = new File( subWatermark, identifier ); } File subSize = new File( subName, Integer.toString( size ) ); // Only enter synchronized if needed if( ! subSize.isDirectory() ){ synchronized( this ){ createDir( cacheDir ); createDir( subSize ); } } return subSize; } private static void createDir( File dir ) throws IOException { if( !dir.exists() ) { boolean ok = dir.mkdirs(); if( ! ok ){ // This is some wired kind of thread issue. Should not happen because of the synchronized // but does anyways. Perhaps some filesystem problem? This prevents senseless error messages if( !dir.exists() ){ throw new IOException( "Cannot create: " + dir.getAbsolutePath() ); } } } if( !dir.isDirectory() ) throw new IOException( "" + dir.getAbsolutePath() + " exists but is no directory" ); } /* * Create hash for watermark currently this is filename but only printable * chars */ private static String hashWatermark( File watermark ) { return watermark.getName().replaceAll( "\\W", "" ).toLowerCase(); } /* * Create hashString for directory hashing */ private static String hashName( String name, int length ) { //E.rr( name, length ); if( length == 0 ) return null; int i = 0; // Remove trailing '0' chars while( i < name.length() - 1 && name.charAt( i ) == '0' ) { i++; } ; if( name.length() > i ) { int endPos = i+length; if( endPos >= name.length() ) endPos = name.length()-1; return name.substring( i, endPos ).toLowerCase(); } else { return "NOHASH"; } } /* private static class Dim { int w, h; } */ private class OldFilenameFilter implements FilenameFilter { @Override public boolean accept( File dir, String name ) { return name.startsWith( PictureBuilder.this.main.getName() ); } } public static final FilenameFilter JPEG = ( dir, name ) -> { return name.length() > 4 && ".jpg".equalsIgnoreCase( name .substring( name.length() - 4 ) ) || name.length() > 5 && ".jpeg".equalsIgnoreCase( name .substring( name.length() - 5 ) ); }; private static class JpegSorter implements Comparator<File>, Serializable { private static final long serialVersionUID = 1L; private String mainFileName; public JpegSorter( String mainFileName ){ this.mainFileName = mainFileName + ".jpg"; } @Override public int compare( File o1, File o2 ) { if( o1.getName().equalsIgnoreCase( mainFileName ) ){ return -1; } if( o2.getName().equalsIgnoreCase( mainFileName ) ){ return 1; } return o1.getName().compareTo( o2.getName() ); } } @Override public String toString(){ StringBuilder result = new StringBuilder(); result.append( "PB:" ); result.append( " identifier: " + identifier ); if( main != null ) result.append( " main: " ) .append( main.getAbsolutePath() ); if( cacheDir != null ) result.append( " cacheDir: " ) .append( cacheDir.getAbsolutePath() ); result.append( " index: " + index ); return result.toString(); } }