public class ThumbnailerManager implements Thumbnailer, ThumbnailerConstants {

	/**
	 * @var Starting estimate of the number of mime types that the thumbnailer can manager
	 */
	private static final int DEFAULT_NB_MIME_TYPES = 40;
	/**
	 * @var Starting estimate of the number of thumbnailers per mime type
	 */
	private static final int DEFAULT_NB_THUMBNAILERS_PER_MIME = 5;
	
	/** 
	 * @var MIME Type for "all MIME" within thumbnailers Hash
	 */
	private static final String ALL_MIME_WILDCARD = "*/*";
	
	/**
	 * @var Width of thumbnail picture to create (in Pixel)
	 */
	private int thumbWidth;
	
	/**
	 * @var Height of thumbnail picture to create (in Pixel)
	 */
	private int thumbHeight;

	/**
	 * @var Options for image resizer (currently unused)
	 */
	private int thumbOptions = 0;
	
	/** Folder under which new thumbnails should be filed */
	private File thumbnailFolder;
	
	/** The logger for this class */
	private static Logger mLog = Logger.getLogger(ThumbnailerManager.class);

	
	/**
	 * Thumbnailers per MIME-Type they accept (ALL_MIME_WILDCARD for all)
	 */
	private ChainedHashMap<String, Thumbnailer> thumbnailers;
	
	/**
	 * All Thumbnailers.
	 */
	private Queue<Thumbnailer> allThumbnailers;
	
	/**
	 * Magic Mime Detection ... a wrapper class to Aperature's Mime thingies.
	 */
	private MimeTypeDetector mimeTypeDetector;

	/**
	 * Initialise Thumbnail Manager
	 */
	public ThumbnailerManager()
	{
		// Execute close() when JVM shuts down (if it wasn't executed before).
		final ThumbnailerManager self = this; 
		Runtime.getRuntime().addShutdownHook(new Thread() {
		    public void run() { IOUtil.quietlyClose(self); }
		});
		
		thumbnailers = new ChainedHashMap<String, Thumbnailer>(DEFAULT_NB_MIME_TYPES, DEFAULT_NB_THUMBNAILERS_PER_MIME);
		allThumbnailers = new LinkedList<Thumbnailer>();
		
		mimeTypeDetector = new MimeTypeDetector();
		
		thumbHeight = THUMBNAIL_DEFAULT_HEIGHT;
		thumbWidth = THUMBNAIL_DEFAULT_WIDTH;
	}
/* currently not used
	private static String generate_hash(String str)
	{
		return StringUtil.transpose_string(StringUtil.md5(str));
	}
*/
	/**
	 * Calculate a thumbnail filename (via hashing).
	 * 
	 * @param input			Input file
	 * @param checkExist	If true: guarantuee that such a filename doesn't exist yet
	 * @return	The chosen filename
	 */
	public File chooseThumbnailFilename(File input, boolean checkExist)
	{
		if (thumbnailFolder == null)
			throw new RuntimeException("chooseThumbnailFilename cannot be run before a first call to setThumbnailFolder()");
		if (input == null)
			throw new NullPointerException("Input file may not be null");
		
		String hash = ""; //"_" + generate_hash(input.getAbsolutePath());
		String prefix = input.getName().replace('.', '_');
		
		int tries = 0;
		String suffix = "";
		File output;
		do
		{
			if (tries > 0)
			{
				int suffix_length = tries / 4 + 1; // Simple (i.e. guessed) heuristic to add randomness if many files have the same name
				suffix = "-" + StringUtil.randomString(suffix_length);
			}
			
			String name = prefix + hash + suffix + ".png";	
			output = new File(thumbnailFolder, name);

			tries++;
		}
		while (checkExist && output.exists());

		return output;
	}
	
	/**
	 * Set the folder where the thumbnails should be generated by default
	 * (if no output file is given).
	 * 
	 * @param thumbnailPath			Path where the future thumbnails will be written to	
	 * @throws FileDoesNotExistException	If the given path is not writeable
	 */
	public void setThumbnailFolder(String thumbnailPath) throws FileDoesNotExistException {
		setThumbnailFolder(new File(thumbnailPath));
	}

	/**
	 * Set the folder where the thumbnails should be generated by default
	 * (if no output file is given).
	 * 
	 * @param thumbnailPath					Path where the future thumbnails will be written to	
	 * @throws FileDoesNotExistException	If the given path is not writeable
	 */
	public void setThumbnailFolder(File thumbnailPath) throws FileDoesNotExistException {
		FileDoesNotExistException.checkWrite(thumbnailPath, "The thumbnail folder", true, true);
		
		thumbnailFolder = thumbnailPath;
	}

	/**
	 * Generate a Thumbnail.
	 * The output file name is generated using a hashing scheme.
	 * It is garantueed that an existing Thumbnail is not overwritten by this.
	 * 
	 * @param 	input	Input file that should be processed.
	 * @return	Name of Thumbnail-File generated.
	 * @throws FileDoesNotExistException
	 * @throws IOException
	 * @throws ThumbnailerException
	 */
	public File createThumbnail(File input) throws FileDoesNotExistException, IOException, ThumbnailerException
	{
		File output = chooseThumbnailFilename(input, true);
		generateThumbnail(input, output);
		
		return output;
	}
	
	/**
	 * Add a Thumbnailer-Class to the list of available Thumbnailers
	 * Note that the order you add Thumbnailers may make a difference:
	 * First added Thumbnailers are tried first, if one fails, the next 
	 * (that claims to be able to treat such a document) is tried.
	 * (Thumbnailers that claim to treat all MIME Types are tried last, though.)
	 * 
	 * @param thumbnailer	Thumbnailer to add.
	 */
	public void registerThumbnailer(Thumbnailer thumbnailer)
	{
		String[] acceptMIME = thumbnailer.getAcceptedMIMETypes();
		if (acceptMIME == null)
			thumbnailers.put(ALL_MIME_WILDCARD, thumbnailer);
		else
		{
			for (String mime: acceptMIME)
				thumbnailers.put(mime, thumbnailer);
		}
		allThumbnailers.add(thumbnailer);
		
		thumbnailer.setImageSize(thumbWidth, thumbHeight, thumbOptions);
	}

	/**
	 * Instead of a deconstructor:
	 * De-initialize ThumbnailManager and its thumbnailers.
	 * 
	 * This functions should be called before termination of the program,
	 * and Thumbnails can't be generated after calling this function.
	 */
	public void close() {
		if (allThumbnailers == null)
			return; // Already closed
		
		for (Thumbnailer thumbnailer: allThumbnailers)
		{
			try {
				thumbnailer.close();
			} catch (IOException e) {
				mLog.error("Error during close of Thumbnailer:", e);
			}
		}
		
		thumbnailers = null;
		allThumbnailers = null;
	}

	/**
	 * Generate a Thumbnail of the input file.
	 * Try all available Thumbnailers and use the first that returns an image.
	 * 
	 * MIME-Detection narrows the selection of Thumbnailers to try:
	 * <li>First all Thumbnailers that declare to accept such a MIME Type are used
	 * <li>Then all Thumbnailers that declare to accept all possible MIME Types.
	 * 
	 * @param 	input		Input file that should be processed
	 * @param 	output		File in which should be written
	 * @param	mimeType	MIME-Type of input file (null if unknown)
	 * @throws 	IOException			If file cannot be read/written.
	 * @throws ThumbnailerException If the thumbnailing process failed
	 * 								(i.e., no thumbnailer could generate an Thumbnail.
	 * 								 The last ThumbnailerException is re-thrown.)
	 */
	public void generateThumbnail(File input, File output, String mimeType) throws IOException, ThumbnailerException {
		FileDoesNotExistException.check(input, "The input file");
		FileDoesNotExistException.checkWrite(output, "The output file", true, false);

		boolean generated = false;
		
		// MIME might be known already (in case of recursive thumbnail managers)
		if (mimeType == null)
		{
			mimeType = mimeTypeDetector.getMimeType(input);
			mLog.debug("Detected Mime-Typ: " + mimeType);
		}

		if (mimeType != null)
			generated = executeThumbnailers(mimeType, input, output, mimeType);
		
		// Try again using wildcard thumbnailers
		if (!generated)
			generated = executeThumbnailers(ALL_MIME_WILDCARD, input, output, mimeType);
		
		if (!generated)
			throw new ThumbnailerException("No suitable Thumbnailer has been found. (File: " + input.getName() + " ; Detected MIME: " + mimeType + ")");
	}

	/**
	 * Generate a Thumbnail of the input file.
	 * Try all available Thumbnailers and use the first that returns an image.
	 * 
	 * MIME-Detection narrows the selection of Thumbnailers to try:
	 * <li>First all Thumbnailers that declare to accept such a MIME Type are used
	 * <li>Then all Thumbnailers that declare to accept all possible MIME Types.
	 * 
	 * @param 	input		Input file that should be processed
	 * @param 	output		File in which should be written
	 * @throws 	IOException			If file cannot be read/written.
	 * @throws ThumbnailerException If the thumbnailing process failed
	 * 								(i.e., no thumbnailer could generate an Thumbnail.
	 * 								 The last ThumbnailerException is re-thrown.)
	 */
	public void generateThumbnail(File input, File output) throws IOException, ThumbnailerException {
		generateThumbnail(input, output, null);
	}

	/**
	 * Helper function for Thumbnail generation:
	 * execute all thumbnailers of a given MimeType.
	 * 
	 * 
	 * @param useMimeType		Which MIME Type the thumbnailers should be taken from
	 * @param input				Input File that should be processed
	 * @param output			Output file where the image shall be written.
	 * @param detectedMimeType	MIME Type that was returned by automatic MIME Detection
	 * @return	True on success (1 thumbnailer could generate the output file).
	 * @throws IOException	Input file cannot be read, or output file cannot be written, or necessary temporary files could not be created.
	 */
	private boolean executeThumbnailers(String useMimeType, File input, File output, String detectedMimeType) throws IOException {
		for (Thumbnailer thumbnailer: thumbnailers.getIterable(useMimeType))
		{
			try {
				thumbnailer.generateThumbnail(input, output, detectedMimeType);
				return true;
			} catch (ThumbnailerException e) {
				// This Thumbnailer apparently wasn't suitable, so try next
				mLog.warn("Warning: " + thumbnailer.getClass().getName() + " could not handle the file " + input.getName() + " (trying next)", e);
			}
		}
		return false;
	}

	/**
	 * Set the image size of all following thumbnails.
	 * 
	 * ThumbnailManager delegates this to all his containing Thumbailers.
	 */
	public void setImageSize(int width, int height, int imageResizeOptions) {
		thumbHeight = height;
		thumbWidth = width;
		thumbOptions = imageResizeOptions;

		if (thumbWidth < 0)
			thumbWidth = 0;
		if (thumbHeight < 0)
			thumbHeight = 0;
		
		for (Thumbnailer thumbnailer: allThumbnailers)
			thumbnailer.setImageSize(thumbWidth, thumbHeight, thumbOptions);
	}
	
	/**
	 * Get the currently set Image Width of this Thumbnailer.
	 * @return	image width of created thumbnails.
	 */
	public int getCurrentImageWidth()
	{
		return thumbWidth;
	}

	/**
	 * Get the currently set Image Height of this Thumbnailer.
	 * @return	image height of created thumbnails.
	 */
	public int getCurrentImageHeight()
	{
		return thumbHeight;
	}
	

	/**
	 * Summarize all contained MIME Type Thumbnailers.
	 * @return All accepted MIME Types, null if any.
	 */
	public String[] getAcceptedMIMETypes() {
		if (thumbnailers.containsKey(ALL_MIME_WILDCARD))
			return null; // All MIME Types
		else
			return thumbnailers.keySet().toArray(new String[]{});
	}

} public abstract class JODConverterThumbnailer extends AbstractThumbnailer {

	/** The logger for this class */
	protected static Logger mLog = Logger.getLogger(JODConverterThumbnailer.class);

	/**
	 * JOD Office Manager
	 */
	protected static OfficeManager officeManager = null;
	
	/**
	 * JOD Converter
	 */
	protected static OfficeDocumentConverter officeConverter = null;

	/**
	 * Thumbnail Extractor for OpenOffice Files
	 */
	protected OpenOfficeThumbnailer ooo_thumbnailer = null;
	
	/**
	 * MimeIdentification
	 */
	protected MimeTypeDetector mimeTypeDetector = null;

	private TemporaryFilesManager temporaryFilesManager = null;

	/**
	 * OpenOffice Home Folder (Configurable)
	 */
	private static String openOfficeHomeFolder = null;
	
	/**
	 * The Port on which to connect (must be unoccupied)
	 */
	private final static int OOO_DEFAULT_PORT = 8100;
	
	/**
	 * OpenOffice Home Folder (Configurable)
	 */
	private static int openOfficePort = OOO_DEFAULT_PORT;
	
	/**
	 * How long may a conversion take? (in ms)
	 */
	private static final long JOD_DOCUMENT_TIMEOUT = 12000;
	
	public static void setOpenOfficePort(int openOfficePort) {
		if (openOfficePort > 0)
			JODConverterThumbnailer.openOfficePort = openOfficePort;
	}

	public static void setOpenOfficeHomeFolder(String openOfficeHomeFolder)
	{
		JODConverterThumbnailer.openOfficeHomeFolder = openOfficeHomeFolder;
	}

	public static void setOpenOfficeProfileFolder(String paramOpenOfficeProfile)
	{
		if (paramOpenOfficeProfile != null)
			JODConverterThumbnailer.openOfficeTemplateProfileDir = new File(paramOpenOfficeProfile);
	}
	
	public JODConverterThumbnailer()
	{
		ooo_thumbnailer = new OpenOfficeThumbnailer();
		mimeTypeDetector = new MimeTypeDetector();
		temporaryFilesManager = new TemporaryFilesManager();
	}
	
	protected static File openOfficeTemplateProfileDir = null;
	
	
	/**
	 * Start OpenOffice-Service and connect to it.
	 * (Does not reconnect if already connected.)
	 */
	public static void connect() { 	connect(false); }
	
	/**
	 * Start OpenOffice-Service and connect to it.
	 * @param forceReconnect	Connect even if he is already connected.
	 */
	public static void connect(boolean forceReconnect)
	{
		if (!forceReconnect && isConnected())
			return;
		
		DefaultOfficeManagerConfiguration config = new DefaultOfficeManagerConfiguration()
			.setPortNumber(openOfficePort)
			.setTaskExecutionTimeout(JOD_DOCUMENT_TIMEOUT);
		
		if (openOfficeHomeFolder != null)
			config.setOfficeHome(openOfficeHomeFolder);
		
		if (openOfficeTemplateProfileDir != null)
		{
			if (openOfficeTemplateProfileDir.exists())
				config.setTemplateProfileDir(openOfficeTemplateProfileDir);
			else
				mLog.info("No Template Profile Folder found at " + openOfficeTemplateProfileDir.getAbsolutePath() + " - Creating temporary one.");
		}
		else
			mLog.info("Creating temporary profile folder...");
			
		officeManager = config.buildOfficeManager();
		officeManager.start();
		
		officeConverter = new OfficeDocumentConverter(officeManager);
	}
	
	/**
	 * Check if a connection to OpenOffice is established.
	 * @return	True if connected.
	 */
	public static boolean isConnected()
	{
		return officeManager != null && officeManager.isRunning();
	}

	/**
	 * Stop the OpenOffice Process and disconnect.
	 */
	public static void disconnect()
	{
		// close the connection
		if (officeManager != null)
			officeManager.stop();
		officeManager = null;
	}
	
	public void close() throws IOException
	{
		try {
			try {
				temporaryFilesManager.deleteAllTempfiles();
				ooo_thumbnailer.close();
			} finally {
				disconnect();
			}
		} finally {
			super.close();
		}
	}	
	
	
	
	/**
	 * Generates a thumbnail of Office files.
	 * 
	 * @param input		Input file that should be processed
	 * @param output	File in which should be written
	 * @throws IOException			If file cannot be read/written
	 * @throws ThumbnailerException If the thumbnailing process failed.
	 */
	@Override
	public void generateThumbnail(File input, File output) throws IOException, ThumbnailerException {
		// Connect on first use
		if (!isConnected())
			connect();

		File outputTmp = null;
		try {
			outputTmp = File.createTempFile("jodtemp", "." + getStandardOpenOfficeExtension());

			// Naughty hack to circumvent invalid URLs under windows (C:\\ ...)
			if (Platform.isWindows())
				input = new File(input.getAbsolutePath().replace("\\\\", "\\"));

			try {
				officeConverter.convert(input, outputTmp);
			} catch (OfficeException e) {
				throw new ThumbnailerException("Could not convert into OpenOffice-File", e);
			}
			if (outputTmp.length() == 0)
			{
				throw new ThumbnailerException("Could not convert into OpenOffice-File (file was empty)...");
			}

			ooo_thumbnailer.generateThumbnail(outputTmp, output);
		} finally {
			IOUtil.deleteQuietlyForce(outputTmp);
		}
	}

	/**
	 * Generate a Thumbnail of the input file.
	 * (Fix file ending according to MIME-Type).
	 * 
	 * @param input		Input file that should be processed
	 * @param output	File in which should be written
	 * @param mimeType	MIME-Type of input file (null if unknown)
	 * @throws IOException			If file cannot be read/written
	 * @throws ThumbnailerException If the thumbnailing process failed.
	 */
	public void generateThumbnail(File input, File output, String mimeType) throws IOException, ThumbnailerException {
		String ext = FilenameUtils.getExtension(input.getName());
		if (!mimeTypeDetector.doesExtensionMatchMimeType(ext, mimeType))
		{
			String newExt;
			if ("application/zip".equals(mimeType))
				newExt = getStandardZipExtension();
			else if ("application/vnd.ms-office".equals(mimeType))
				newExt = getStandardOfficeExtension();
			else
				newExt = mimeTypeDetector.getStandardExtensionForMimeType(mimeType);
			
			input = temporaryFilesManager .createTempfileCopy(input, newExt);
		}

		generateThumbnail(input, output);
	}
	
	protected abstract String getStandardZipExtension();
	protected abstract String getStandardOfficeExtension();
	protected abstract String getStandardOpenOfficeExtension();

	public void setImageSize(int thumbWidth, int thumbHeight, int imageResizeOptions) {
		super.setImageSize(thumbWidth, thumbHeight, imageResizeOptions);
		ooo_thumbnailer.setImageSize(thumbWidth, thumbHeight, imageResizeOptions);
	}
} public class JODExcelConverterThumbnailer extends JODConverterThumbnailer {

	public JODExcelConverterThumbnailer() throws IOException { super(); }

	protected String getStandardOpenOfficeExtension() {
		return "ods";
	}

	protected String getStandardZipExtension() {
		return "xlsx";
	}
	protected String getStandardOfficeExtension() {
		return "xls";
	}

	/**
     * Get a List of accepted File Types.
     * All Spreadsheet Office Formats that OpenOffice understands are accepted.
     * 
     * @return MIME-Types
     * @see http://www.artofsolving.com/opensource/jodconverter/guide/supportedformats
     */
	public String[] getAcceptedMIMETypes()
	{
		return new String[]{
				"application/vnd.ms-excel",
				"application/vnd.openxmlformats-officedocument.spreadsheetml",
				"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		/*		"application/vnd.ms-office", // xls?
				"application/zip" // xlsx? */
		};
	}


	
} public class JODHtmlConverterThumbnailer extends JODConverterThumbnailer {

	public JODHtmlConverterThumbnailer() throws IOException { super(); }

	protected String getStandardOpenOfficeExtension() {
		return ".odt";
	}

	protected String getStandardZipExtension() {
		return "html";
	}
	protected String getStandardOfficeExtension() {
		return "html";
	}

    /**
     * Get a List of accepted File Types.
     * All Text Office Formats that OpenOffice understands are accepted.
     * (txt, rtf, doc, docx, wpd)
     * 
     * @return MIME-Types
     * @see http://www.artofsolving.com/opensource/jodconverter/guide/supportedformats
     */
	public String[] getAcceptedMIMETypes()
	{
		return new String[]{
				"text/html"
		};
	}
	
} public class JODWordConverterThumbnailer extends JODConverterThumbnailer {

	public JODWordConverterThumbnailer() throws IOException { super(); }

	protected String getStandardOpenOfficeExtension() {
		return ".odt";
	}

	protected String getStandardZipExtension() {
		return "docx";
	}
	protected String getStandardOfficeExtension() {
		return "doc";
	}

    /**
     * Get a List of accepted File Types.
     * All Text Office Formats that OpenOffice understands are accepted.
     * (txt, rtf, doc, docx, wpd)
     * 
     * @return MIME-Types
     * @see http://www.artofsolving.com/opensource/jodconverter/guide/supportedformats
     */
	public String[] getAcceptedMIMETypes()
	{
		return new String[]{
				"text/plain",
				"text/rtf",
/*				"application/msword", */
				"application/vnd.ms-word",
				"application/vnd.openxmlformats-officedocument.wordprocessingml",
				"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
				"application/wordperfect",
		/*		"application/vnd.ms-office", // doc?
				"application/zip" // docx? */
		};
	}
	
} public class NativeImageThumbnailer extends AbstractThumbnailer {

	public void generateThumbnail(File input, File output) throws IOException, ThumbnailerException {
		ResizeImage resizer = new ResizeImage(thumbWidth, thumbHeight);
		
		try {
			resizer.setInputImage(input);
		} catch (UnsupportedInputFileFormatException e) {
			throw new ThumbnailerException("File format could not be interpreted as image", e);
		}
		resizer.writeOutput(output);		
	}

    /**
     * Get a List of accepted File Types.
     * Normally, these are: bmp, jpg, wbmp, jpeg, png, gif
     * The exact list may depend on the Java installation.
     * 
     * @return MIME-Types
     */
	public String[] getAcceptedMIMETypes()
	{
		return ImageIO.getReaderMIMETypes();
	}
} public class OpenOfficeThumbnailer extends AbstractThumbnailer {

	@Override
	public void generateThumbnail(File input, File output) throws IOException, ThumbnailerException {
		BufferedInputStream in = null;
		ZipFile zipFile = null;
		
		try {
			 zipFile = new ZipFile(input);
		} catch (ZipException e) {
			throw new ThumbnailerException("This is not a zipped file. Is this really an OpenOffice-File?", e);
		}
		
		try {
			ZipEntry entry = zipFile.getEntry("Thumbnails/thumbnail.png");
			if (entry == null)
				throw new ThumbnailerException("Zip file does not contain 'Thumbnails/thumbnail.png' . Is this really an OpenOffice-File?");
			
			in = new BufferedInputStream(zipFile.getInputStream(entry));				

			ResizeImage resizer = new ResizeImage(thumbWidth, thumbHeight);
			resizer.setInputImage(in);
			resizer.writeOutput(output);
			
			in.close();
		}
		finally {
			IOUtil.quietlyClose(in);
			IOUtil.quietlyClose(zipFile);
		}
	}
	
    /**
     * Get a List of accepted File Types.
     * All OpenOffice Formats are accepted.
     * 
     * @return MIME-Types
     */
	public String[] getAcceptedMIMETypes()
	{
		return new String[] {
			      "application/vnd.sun.xml.writer",
			      "application/vnd.sun.xml.writer.template",
			      "application/vnd.sun.xml.writer.global",
			      "application/vnd.sun.xml.calc",
			      "application/vnd.sun.xml.calc.template",
			      "application/vnd.stardivision.calc",
			      "application/vnd.sun.xml.impress",
			      "application/vnd.sun.xml.impress.template ",
			      "application/vnd.stardivision.impress sdd",
			      "application/vnd.sun.xml.draw",
			      "application/vnd.sun.xml.draw.template",
			      "application/vnd.stardivision.draw",
			      "application/vnd.sun.xml.math",
			      "application/vnd.stardivision.math",
			      "application/vnd.oasis.opendocument.text",
			      "application/vnd.oasis.opendocument.text-template",
			      "application/vnd.oasis.opendocument.text-web",
			      "application/vnd.oasis.opendocument.text-master",
			      "application/vnd.oasis.opendocument.graphics",
			      "application/vnd.oasis.opendocument.graphics-template",
			      "application/vnd.oasis.opendocument.presentation",
			      "application/vnd.oasis.opendocument.presentation-template",
			      "application/vnd.oasis.opendocument.spreadsheet",
			      "application/vnd.oasis.opendocument.spreadsheet-template",
			      "application/vnd.oasis.opendocument.chart",
			      "application/vnd.oasis.opendocument.formula",
			      "application/vnd.oasis.opendocument.database",
			      "application/vnd.oasis.opendocument.image",
			      
			      "application/zip" /* Could be an OpenOffice file! */
		};
	}

} public class PDFBoxThumbnailer extends AbstractThumbnailer {

	private static final Color TRANSPARENT_WHITE = new Color(255, 255, 255, 0);
	
	@Override
	public void generateThumbnail(File input, File output) throws IOException,
			ThumbnailerException {
		FileDoesNotExistException.check(input);
		if (input.length() == 0)
			throw new FileDoesNotExistException("File is empty");
		FileUtils.deleteQuietly(output);

		PDDocument document = null;
		try
		{
			try {
				document = PDDocument.load(input);
			} catch (IOException e) {
				throw new ThumbnailerException("Could not load PDF File", e);
			}

			BufferedImage tmpImage = writeImageFirstPage(document, BufferedImage.TYPE_INT_RGB);

			if (tmpImage.getWidth() == thumbWidth)
			{
				ImageIO.write(tmpImage, "PNG", output);
			}
			else
			{
				ResizeImage resizer = new ResizeImage(thumbWidth, thumbHeight);
				resizer.resizeMethod = ResizeImage.NO_RESIZE_ONLY_CROP;
				resizer.setInputImage(tmpImage);
				resizer.writeOutput(output);				
			}
		}
		finally
		{
			if( document != null )
			{
				try {
					document.close();
				} catch (IOException e)  {}
			}
		}
	}
	
	/**
	 * Loosely based on the commandline-Tool PDFImageWriter
	 * @param document
	 * @param imageType
	 * @return
	 * @throws IOException
	 */
    private BufferedImage writeImageFirstPage(PDDocument document, int imageType)
    throws IOException
    {
    	List<?> pages = document.getDocumentCatalog().getAllPages();

    	PDPage page = (PDPage)pages.get(0);

    	// resolution: Unfortunately, the resolution is in integer in the call ... so we approximate by taking slightly less (rounding down).
    	/* Before:
    	PDRectangle rect = page.getMediaBox();
    	float resolution = (thumb_width / rect.getWidth() * 72);
    	page.convertToImage(imageType, (int) resolution);
    	*/

    	// Here is the main work:
    	BufferedImage image = convertToImage(page, imageType, thumbWidth, thumbHeight); 
    	
    	return image;
    }
    
    /*     */   private BufferedImage convertToImage(PDPage page, int imageType, int thumbWidth, int thumbHeight)
    /*     */     throws IOException
    /*     */   {
    /* 707 */     PDRectangle mBox = page.findMediaBox();
    /* 708 */     float widthPt = mBox.getWidth();
    /* 709 */     float heightPt = mBox.getHeight();
    /* 711 */     int widthPx = thumbWidth; 			// Math.round(widthPt * scaling);
    /* 712 */     int heightPx = thumbHeight; 			// Math.round(heightPt * scaling);
    /* 710 */     float scaling = thumbWidth / widthPt; // resolution / 72.0F;
    /*     */ 
    /* 714 */     Dimension pageDimension = new Dimension((int) widthPt, (int) heightPt);
    /*     */ 
    /* 716 */     BufferedImage retval = new BufferedImage(widthPx, heightPx, imageType);
    /* 717 */     Graphics2D graphics = (Graphics2D)retval.getGraphics();
    /* 718 */     graphics.setBackground(TRANSPARENT_WHITE);
    /* 719 */     graphics.clearRect(0, 0, retval.getWidth(), retval.getHeight());
    /* 720 */     graphics.scale(scaling, scaling);
    /* 721 */     PageDrawer drawer = new PageDrawer();
    /* 722 */     drawer.drawPage(graphics, page, pageDimension);
    /*     */     try
    /*     */     {
    /* 728 */       int rotation = page.findRotation();
    /* 729 */       if ((rotation == 90) || (rotation == 270))
    /*     */       {
    /* 731 */         int w = retval.getWidth();
    /* 732 */         int h = retval.getHeight();
    /* 733 */         BufferedImage rotatedImg = new BufferedImage(w, h, retval.getType());
    /* 734 */         Graphics2D g = rotatedImg.createGraphics();
    /* 735 */         g.rotate(Math.toRadians(rotation), w / 2, h / 2);
    /* 736 */         g.drawImage(retval, null, 0, 0);
    /*     */       }
    /*     */     }
    /*     */     catch (ImagingOpException e)
    /*     */     {
    /* 741 */       //log.warn("Unable to rotate page image", e);
    /*     */     }
    /*     */ 
    /* 744 */     return retval;
    /*     */   }

    /**
     * Get a List of accepted File Types.
     * Only PDF Files are accepted.
     * 
     * @return MIME-Types
     */
	public String[] getAcceptedMIMETypes()
	{
		return new String[]{
				"application/pdf"
		};
	}

    
} public class ScratchThumbnailer extends AbstractThumbnailer {
	
	/**
	 * Generate a thumbnail from a Scratch file.
	 * 
	 * Adapted from GetThumbnail version 1.1, Jan Rochat 2009
	 * @see http://scratch.mit.edu/forums/viewtopic.php?id=13463
	 * 
	 * @param input		Scratch file to process
	 * @param output 	Where to save the thumbnail
	 * @throws ThumbnailerException If input is not a scratch file
	 * @throws IOException			If output cannot be written.
	 */
	public void generateThumbnail(File input, File output) throws ThumbnailerException, IOException
	{
		FileInputStream in = null;
		try {
			in = new FileInputStream(input);
			ObjReader reader = new ObjReader(in);
	
			Hashtable<?,?> parsedScratchFile;
			try {
				 parsedScratchFile = reader.readInfo();
			} catch (IOException e) {
				throw new ThumbnailerException("Error - Is this really a scratch project file?");
			}
			
			BufferedImage image = (BufferedImage) parsedScratchFile.get("thumbnail");

			/* Output internal data of Scratch files for debug purposes
			
			debugOutputHashtable(parsedScratchFile);
			
			// Rewind
			in = new FileInputStream(input);
			reader = new ObjReader(in);
			
			debugOutputObjects(reader.readObjects(null));
			*/

			ResizeImage imageResizer = new ResizeImage(thumbWidth, thumbHeight);
			imageResizer.setInputImage(image);
			imageResizer.writeOutput(output);
		} finally {
			IOUtil.quietlyClose(in);
		}
	}
	
	@SuppressWarnings("unused")
	private void debugOutputObjects(Object[][] readObjects) {
		for (int i = 0; i < readObjects.length; i++)
		{
			for (int j = 0; j < readObjects[i].length; j++)
			{
				String str = readObjects[i][j].toString();
				if (!(readObjects[i][j] instanceof String))
					str = readObjects[i][j].getClass().getName() + ":" + str;
				System.out.println("obj[" + i + "][" + j + "]: " + readObjects[i][j]);
			}
		}
	}

	@SuppressWarnings("unused")
	private void debugOutputHashtable(Hashtable<?, ?> parsedScratchFile) {
		for (Entry<?, ?> key : parsedScratchFile.entrySet())
		{
			System.out.println("Key: " + key.getKey() + " Value: " + key.getValue());
		}
	}

	/**
	 * Get a list of all MIME Types that this Thumbnailer is ready to process.
	 * You should override this method in order to give hints when which Thumbnailer is most appropriate.
	 * If you do not override this method, the Thumbnailer will be called in any case - awaiting a ThumbnailException if
	 * this thumbnailer cannot treat such a file.
	 * 
	 * @return List of MIME Types. If null, all Files may be passed to this Thumbnailer.
	 */	
	public String[] getAcceptedMIMETypes()
	{
		return new String[]{ScratchFileIdentifier.SCRATCH_MIME_TYPE};
	}
} public class MimeTypeDetector {

	private MagicMimeTypeIdentifier mimeTypeIdentifier;

	private List<MimeTypeIdentifier> extraIdentifiers;
	
	private static Logger mLog = Logger.getLogger(MimeTypeDetector.class);

	/**
	 * Create a MimeType Detector and init it.
	 */
	public MimeTypeDetector()
	{
		MagicMimeTypeIdentifierFactory mimeTypeFactory = new MagicMimeTypeIdentifierFactory();
		mimeTypeIdentifier = (MagicMimeTypeIdentifier) mimeTypeFactory.get();
		
		extraIdentifiers = new ArrayList<MimeTypeIdentifier>();
		
		addMimeTypeIdentifier(new ScratchFileIdentifier());
		addMimeTypeIdentifier(new Office2007FileIdentifier());
		addMimeTypeIdentifier(new PptFileIdentifier());
		addMimeTypeIdentifier(new XlsFileIdentifier());
		addMimeTypeIdentifier(new DocFileIdentifier());
	}

	/**
	 * Add a new MimeTypeIdentifier to this Detector.
	 * MimeTypeIdentifier may override the decision of the detector.
	 * The order the identifiers are added will also be the order they will be executed
	 * (i.e., the last identifiers may override all others.)
	 * 
	 * @param identifier	a new MimeTypeIdentifier
	 */
	public void addMimeTypeIdentifier(MimeTypeIdentifier identifier)
	{
		extraIdentifiers.add(identifier);
	}
	
	/**
	 * Detect MIME-Type for this file.
	 * 
	 * @param file	File to analyse
	 * @return	String of MIME-Type, or null if no detection was possible (or unknown MIME Type)
	 */
	public String getMimeType(File file)
	{
		byte[] bytes = new byte[mimeTypeIdentifier.getMinArrayLength()];
		
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			fis.read(bytes);
			fis.close();
			fis = null;
		} catch (IOException e) {
			return null; // File does not exist or other I/O Error
		} finally {
			IOUtil.quietlyClose(fis);
		}
		String file_url = file.toURI().toASCIIString();
		String mimeType = mimeTypeIdentifier.identify(bytes, file.getPath(), new URIImpl(file_url)) ;

		/* I don't see any effect of this
		if (mimeType != null && mimeType.equalsIgnoreCase("application/zip")) {
			mLog.info("Is a zip-file. Try second round-detection ...");
			// some new files like MS Office documents are zip files
			// so rewrite the URL for the correct mimetype detection
			mimeType = mimeTypeIdentifier.identify(bytes, null, new URIImpl("zip:mime:" + file_url));
		}
		*/
		
		if (mimeType != null && mimeType.length() == 0)
			mimeType = null;
		
		// Identifiers may re-write MIME.
		for (MimeTypeIdentifier identifier : extraIdentifiers)
			mimeType = identifier.identify(mimeType, bytes, file);
		
		mLog.info("Detected MIME-Type of " + file.getName() + " is " + mimeType);
		return mimeType;
	}
	
	/**
	 * Return the standard extension of a specific MIME-Type.
	 * What are these files "normally" called?
	 * 
	 * @param mimeType	MIME-Type, e.g. "text/plain"
	 * @return	Extension, e.g. "txt"
	 */
	public String getStandardExtensionForMimeType(String mimeType)
	{
		List<String> extensions = getExtensionsCached(mimeType);
		
		if (extensions == null)
			return null;
		
		try {
			return extensions.get(0);
		} catch (IndexOutOfBoundsException e) {
			return null;
		}
	}

	Map<String, List<String>> extensionsCache = new HashMap<String, List<String>>(); 
	
	@SuppressWarnings("unchecked")
	protected List<String> getExtensionsCached(String mimeType) {
		List<String> extensions = extensionsCache.get(mimeType);
		if (extensions != null)
			return extensions;
		
		extensions = (List<String>) mimeTypeIdentifier.getExtensionsFor(mimeType);
		
		for (MimeTypeIdentifier identifier : extraIdentifiers)
		{
			if (extensions != null)
				return extensions;
			
			extensions = identifier.getExtensionsFor(mimeType);
		}
		
		extensionsCache.put(mimeType, extensions);
		return extensions;
	}
	
	/**
	 * Test if an given extension can contain a File of MIME-Type
	 * @param extension	Filename extension (e.g. "txt")
	 * @param mimeType	MIME-Type		   (e.g. "text/plain")
	 * @return	True if compatible.
	 */
	public boolean doesExtensionMatchMimeType(String extension, String mimeType)
	{
		List<String> extensions = getExtensionsCached(mimeType); 
		
		if (extensions == null)
			return false;
	
		return extensions.contains(extension);
	}
}
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.zip.DataFormatException;
import org.apache.log4j.Logger;
import org.apache.lucene.document.CompressionTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import de.uni_siegen.wineme.come_in.thumbnailer.FileDoesNotExistException;
import de.uni_siegen.wineme.come_in.thumbnailer.ThumbnailerConstants;
import de.uni_siegen.wineme.come_in.thumbnailer.ThumbnailerException;
import de.uni_siegen.wineme.come_in.thumbnailer.ThumbnailerManager;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.JODConverterThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.JODExcelConverterThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.JODHtmlConverterThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.JODPowerpointConverterThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.JODWordConverterThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.NativeImageThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.OpenOfficeThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.PDFBoxThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.thumbnailers.ScratchThumbnailer;
import de.uni_siegen.wineme.come_in.thumbnailer.util.IOUtil;
import de.uni_siegen.wineme.come_in.thumbnailer.util.mime.MimeTypeDetector;
import net.sf.regain.RegainException;
import net.sf.regain.crawler.Crawler;
import net.sf.regain.crawler.config.PreparatorConfig;
import net.sf.regain.crawler.document.RawDocument;
import net.sf.regain.crawler.document.WriteablePreparator;
import net.sf.regain.crawler.plugin.AbstractCrawlerPlugin;
/*
 * regain/Thumbnailer - A file search engine providing plenty of formats (Plugin)
 * Copyright (C) 2011  Come_IN Computerclubs (University of Siegen)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Contact: Come_IN-Team <come_in-team@listserv.uni-siegen.de>
 */

package de.uni_siegen.wineme.come_in.thumbnailer.plugin;





/**
 * Integration of Thumbnailer into Regain
 * 
 * In order to save ressources, the Thumbnail Library remains loaded only until the end of the crawling process.
 * 
 * @author Benjamin
 */
public class ThumbnailerPlugin extends AbstractCrawlerPlugin implements ThumbnailerLuceneConstants, ThumbnailerConstants
{
	/**
	 * @var Logger instance
	 */
	private static Logger mLog = Logger.getLogger(ThumbnailerPlugin.class);

	/**
	 * @var Thumbnailer Manager (can be null if deactivated)
	 */
	private ThumbnailerManager thumbnailer;
	
	/**
	 * Keep track of all files that where generated during a crawling process,
	 * in order to delete those which didn't reach the regain index.
	 */
	private Collection<String> generatedThumbnailsNotIndexed;
	
	/**
	 * @var If thumbnails should be generated.
	 * This can be deactivated due to initialization errors, or via config.
	 */
	private boolean thumbnailGenerationDeactivated = false;

	/**
	 * Parameters that were found in the XML Configuration
	 */
	private int paramThumbnailWidth;
	private int paramThumbnailHeight;
	private File paramThumbnailFolder;
	private String paramOpenOfficeHome;
	private String paramOpenOfficeProfile;
	private int paramOpenOfficePort;

	private MimeTypeDetector mimeTypeDetector;
		
	/**
	 * Initializes the plugin.
	 *
	 * @param 	config 	The configuration for this plugin.
	 * @throws RegainException When the configuration has an error.
	 */
	@Override
	public void init(PreparatorConfig config) throws RegainException {
		Map<String, String> thumbnailConfig = config.getSectionWithName("thumbnailing");
		
		String paramThumbnailFolderStr = null;
		if (thumbnailConfig != null)
			paramThumbnailFolderStr = thumbnailConfig.get("thumbnailFolder");
		
		if (paramThumbnailFolderStr == null || paramThumbnailFolderStr.isEmpty())
		{
			mLog.warn("Thumbnail folder is not given; using default value (thumbs/)");
			paramThumbnailFolderStr = "thumbs/";
		}
		paramThumbnailFolder = new File(paramThumbnailFolderStr);
		
		if (thumbnailConfig != null)
		{
			try {
				paramThumbnailWidth = Integer.parseInt(thumbnailConfig.get("imageWidth"));
				paramThumbnailHeight = Integer.parseInt(thumbnailConfig.get("imageHeight"));
			} catch (NumberFormatException e) {
				mLog.warn("Could not parse desired thumbnail height/width (are these really integers?); using default values (" + THUMBNAIL_DEFAULT_WIDTH + "x" + THUMBNAIL_DEFAULT_WIDTH +")", e);
			}
		}
		if (paramThumbnailHeight <= 0)
		{
			mLog.warn("Invalid value for thumbnail height (" + paramThumbnailHeight + "): taking default " + THUMBNAIL_DEFAULT_WIDTH + "x" + THUMBNAIL_DEFAULT_WIDTH);
			paramThumbnailWidth = THUMBNAIL_DEFAULT_WIDTH;
			paramThumbnailHeight = THUMBNAIL_DEFAULT_HEIGHT;				
		} else if (paramThumbnailWidth <= 0) {
			mLog.warn("Invalid value for thumbnail width (" + paramThumbnailWidth + "): taking default " + THUMBNAIL_DEFAULT_WIDTH + "x" + THUMBNAIL_DEFAULT_WIDTH);
			paramThumbnailWidth = THUMBNAIL_DEFAULT_WIDTH;
			paramThumbnailHeight = THUMBNAIL_DEFAULT_HEIGHT;				
		}
		
		Map<String, String> externalConfig = config.getSectionWithName("externalHelpers");
		if (externalConfig != null)
		{
			paramOpenOfficeHome = externalConfig.get("openOfficeHome");
			if (paramOpenOfficeHome != null && !(new File(paramOpenOfficeHome).exists()) )
			{
				mLog.error("ERROR: Could not find OpenOffice-Installation: The specified directory does not exist. Trying to auto-detect.");
				paramOpenOfficeHome = null;
			}
			
			paramOpenOfficeProfile = externalConfig.get("openOfficeProfile");

			try {
				if (externalConfig.get("openOfficePort") != null)
					paramOpenOfficePort = Integer.parseInt(externalConfig.get("openOfficePort"));
			} catch (NumberFormatException e) {
				mLog.error("ERROR: OpenOfficePort is not a number");
			}
		}
		
		JODConverterThumbnailer.setOpenOfficeHomeFolder(paramOpenOfficeHome);
		JODConverterThumbnailer.setOpenOfficeProfileFolder(paramOpenOfficeProfile);
		JODConverterThumbnailer.setOpenOfficePort(paramOpenOfficePort);
		
		mimeTypeDetector = new MimeTypeDetector();
		
		generatedThumbnailsNotIndexed = new HashSet<String>();
	}
	
	/**
	 * Called before the crawling process starts (Crawler::run()).
	 * Initialize Thumbnail Generation:
	 * load Config and configure Thumbnailer
	 * 
	 * This may be called multiple times during the lifetime of a plugin instance,
	 * but CrawlerPlugin::onFinishCrawling() is always called in between.
	 * 
	 * @param crawler 		The crawler instance that is about to begin crawling
	 */
	public void onStartCrawling(Crawler crawler) {
		if (thumbnailGenerationDeactivated)
		{
			mLog.info("Do not start thumbnail generation (deactivated)");
			return;
		}	

		mLog.info("Initialize Thumbnail Generation...");
		thumbnailer = new ThumbnailerManager(); 
		
		thumbnailer.setImageSize(paramThumbnailWidth, paramThumbnailHeight, 0);
		try {
			thumbnailer.setThumbnailFolder(paramThumbnailFolder);
		} catch (FileDoesNotExistException e) {
			mLog.error("Could not set Thumbnail Directory:", e);
			thumbnailer = null;
			mLog.warn("Thumbnailer Plugin was deactivated due to prior errors");
			return;
		}

		try {
			thumbnailer.registerThumbnailer(new NativeImageThumbnailer());
	
			thumbnailer.registerThumbnailer(new OpenOfficeThumbnailer());
			thumbnailer.registerThumbnailer(new PDFBoxThumbnailer());
			try {
				thumbnailer.registerThumbnailer(new JODWordConverterThumbnailer());