package de.wolutz.oooutils;

/*
 * A tool to analyse office-documents and scan for basic-modules and functions.
 * Copyright (C) 2006 Christoph Lutz
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import com.sun.star.awt.FontWeight;
import com.sun.star.beans.PropertyValue;
import com.sun.star.beans.XPropertySet;
import com.sun.star.comp.helper.Bootstrap;
import com.sun.star.container.NoSuchElementException;
import com.sun.star.container.XNameContainer;
import com.sun.star.document.MacroExecMode;
import com.sun.star.frame.XComponentLoader;
import com.sun.star.frame.XDesktop;
import com.sun.star.frame.XModel;
import com.sun.star.lang.IllegalArgumentException;
import com.sun.star.lang.IndexOutOfBoundsException;
import com.sun.star.lang.WrappedTargetException;
import com.sun.star.script.XStarBasicAccess;
import com.sun.star.script.XStarBasicLibraryInfo;
import com.sun.star.script.XStarBasicModuleInfo;
import com.sun.star.sheet.XSheetCellCursor;
import com.sun.star.sheet.XSpreadsheet;
import com.sun.star.sheet.XSpreadsheetDocument;
import com.sun.star.sheet.XSpreadsheetView;
import com.sun.star.sheet.XSpreadsheets;
import com.sun.star.table.XCell;
import com.sun.star.table.XColumnRowRange;
import com.sun.star.text.XTextRange;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;
import com.sun.star.util.URL;
import com.sun.star.util.XCloseable;
import com.sun.star.util.XURLTransformer;

public class OOoMacroScanner {

	public static XComponentLoader xLoader = null;

	public static XURLTransformer xUrlTrans = null;

	public static XSheetCellCursor docCursor = null;

	public static XSheetCellCursor modCursor = null;

	public static XSheetCellCursor subCursor = null;

	public static XSheetCellCursor codeCursor = null;

	public static int docCount = 0;

	public static int modCount = 0;

	public static int subCount = 0;

	public static int codeCount = 0;

	public static HashMap codeRefs = null;

	public static HashMap codeMap = null;

	public static void main(String[] args) throws java.lang.Exception {
		System.out.println("OOoMacroScanner Version 0.1\n"
				+ "Copyright (C) 2006 Christoph Lutz (www.wolutz.de)\n"
				+ "This is free software.\n");
		if (args.length == 0) {
			System.err
					.println("Usage: OOoMacroScanner <list of files or directories>\n");
			System.exit(1);
		}

		System.out.println("Connecting to OpenOffice.org...");
		XComponentContext ctx = Bootstrap.bootstrap();

		XDesktop xDesktop = (XDesktop) UnoRuntime.queryInterface(
				XDesktop.class, ctx.getServiceManager()
						.createInstanceWithContext(
								"com.sun.star.frame.Desktop", ctx));
		xLoader = (XComponentLoader) UnoRuntime.queryInterface(
				XComponentLoader.class, xDesktop);

		xUrlTrans = (XURLTransformer) UnoRuntime.queryInterface(
				XURLTransformer.class, ctx.getServiceManager()
						.createInstanceWithContext(
								"com.sun.star.util.URLTransformer", ctx));

		// initialize codeRef-Counter and codeMap:
		codeRefs = new HashMap();
		codeMap = new HashMap();

		// initialize result-Spreadsheet
		initializeResultSpreadsheet();

		// do the scan-job
		for (int i = 0; i < args.length; i++) {
			File file = new File(args[i]);
			scan(file);
		}

		// write Code-Sheet:
		Iterator iter = codeRefs.keySet().iterator();
		while (iter.hasNext()) {
			String md5 = (String) iter.next();
			int refsCount = ((Integer) codeRefs.get(md5)).intValue();
			String code = codeMap.get(md5).toString();
			String[] lines = code.split("(\r\n|\n\r|\r|\n)");
			writeCodesLine(md5, lines.length, refsCount, code);
		}
		setHeight(codeCursor, 10);
		setOptimalWidth(codeCursor, true);

		// finish
		System.out.println("\nScan finished!");
		System.exit(0);
	}

	private static void initializeResultSpreadsheet()
			throws com.sun.star.io.IOException, IllegalArgumentException,
			NoSuchElementException, WrappedTargetException {
		Object o = xLoader.loadComponentFromURL("private:factory/scalc",
				"_blank", 0, new PropertyValue[] {});
		XSpreadsheetDocument xSPDoc = (XSpreadsheetDocument) UnoRuntime
				.queryInterface(XSpreadsheetDocument.class, o);
		XModel xModel = (XModel) UnoRuntime.queryInterface(XModel.class, o);
		XSpreadsheetView view = null;
		if (xModel != null)
			view = (XSpreadsheetView) UnoRuntime.queryInterface(
					XSpreadsheetView.class, xModel.getCurrentController());

		// create output spreadsheet-document
		if (xSPDoc != null) {
			XSpreadsheets sheets = xSPDoc.getSheets();
			String[] unusedSheets = sheets.getElementNames();

			// create new Sheets and sheetCursor
			sheets.insertNewByName("Code", (short) 0);
			sheets.insertNewByName("Functions", (short) 0);
			sheets.insertNewByName("Modules", (short) 0);
			sheets.insertNewByName("Documents", (short) 0);

			XSpreadsheet sheet = null;

			sheet = (XSpreadsheet) UnoRuntime.queryInterface(
					XSpreadsheet.class, sheets.getByName("Documents"));
			if (sheet != null) {
				docCursor = sheet.createCursor();
				writeHeadings(docCursor, new String[] { "Document",
						"Lines Of Code", "MD5Sum" });
				docCount++;
			}
			if (view != null)
				view.setActiveSheet(sheet);

			sheet = (XSpreadsheet) UnoRuntime.queryInterface(
					XSpreadsheet.class, sheets.getByName("Modules"));
			if (sheet != null) {
				modCursor = sheet.createCursor();
				writeHeadings(modCursor, new String[] { "Document", "Library",
						"Module", "Lines Of Code", "MD5Sum" });
				modCount++;
			}

			sheet = (XSpreadsheet) UnoRuntime.queryInterface(
					XSpreadsheet.class, sheets.getByName("Functions"));
			if (sheet != null) {
				subCursor = sheet.createCursor();
				writeHeadings(subCursor, new String[] { "Document", "Library",
						"Module", "Function", "Lines Of Code", "MD5Sum" });
				subCount++;
			}

			sheet = (XSpreadsheet) UnoRuntime.queryInterface(
					XSpreadsheet.class, sheets.getByName("Code"));
			if (sheet != null) {
				codeCursor = sheet.createCursor();
				writeHeadings(codeCursor, new String[] { "MD5Sum",
						"Lines Of Code", "Reference-Count", "Basic-Code" });
				codeCount++;
			}

			// remove unused sheets
			for (int i = 0; i < unusedSheets.length; i++) {
				sheets.removeByName(unusedSheets[i]);
			}
		}
	}

	public static void scan(File file) {
		if (!file.exists() || !file.canRead()) {
			System.err.println("Skipping file or directory \""
					+ file.getAbsolutePath()
					+ "\" as it does not exist or is not readable.");
			return;
		}
		if (file.isDirectory()) {
			System.out.println("Scanning directory \"" + file.getAbsolutePath()
					+ "\"");
			File[] files = file.listFiles();
			for (int i = 0; i < files.length; i++) {
				scan(files[i]);
			}
			return;
		}

		// get type of document-format
		String codeType = "unknown";
		if (file.getName().toLowerCase().matches(".*\\.(doc|dot|xls|xlt)$"))
			codeType = "VBA";
		if (file.getName().toLowerCase().matches(".*\\.(odt|ott|ods|ots)$"))
			codeType = "StarBasic";
		if (codeType.equals("unknown")) {
			System.err.println("Skipping file \"" + file.getAbsolutePath()
					+ "\" as ist not a documenttype that is readable by OOo "
					+ "(expected: .doc|.dot|.xls|.xlt|.odt|.ott|.ods|.ots)");
			return;
		}

		// now the file is a valid OOo-Document
		System.out.println("Scanning document \"" + file.getPath() + "\"");

		// create md5 of document.
		byte[] buf = new byte[1024];
		String md5 = "";
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			FileInputStream fis = new FileInputStream(file);
			for (int bytes; (bytes = fis.read(buf)) > 0;) {
				md.update(buf, 0, bytes);
			}
			byte[] md5bytes = md.digest();
			for (int k = 0; k < md5bytes.length; k++) {
				byte b = md5bytes[k];
				String str = Integer.toHexString(b + 512);
				md5 += str.substring(1);
			}
			fis.close();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// create a URL that is readable by OOo
		URL[] url = new URL[] { new URL() };
		try {
			url[0].Complete = file.toURL().toString();
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return;
		}
		xUrlTrans.parseStrict(url);
		String urlStr = url[0].Complete;

		// open a hidden document as Template and with macro-execution disabled
		Object doc = null;
		PropertyValue[] props = new PropertyValue[] { new PropertyValue(),
				new PropertyValue(), new PropertyValue() };
		props[0].Name = "MacroExecutionMode";
		props[0].Value = new Short(MacroExecMode.NEVER_EXECUTE);
		props[1].Name = "AsTemplate";
		props[1].Value = Boolean.TRUE;
		props[2].Name = "Hidden";
		props[2].Value = Boolean.TRUE;
		try {
			doc = xLoader.loadComponentFromURL(urlStr, "_blank", 0, props);
		} catch (Exception e) {
			e.printStackTrace();
			return;
		}

		// scan for modules
		int loc = scanDocumentForModules(file.getPath(), codeType, doc);

		// close the document
		XCloseable xCloseable = (XCloseable) UnoRuntime.queryInterface(
				XCloseable.class, doc);
		if (xCloseable != null) {
			try {
				xCloseable.close(false);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		// write results to spreadsheet
		writeDocumentsLine(file.getPath(), loc, md5);
		
		// set optimal widths in result spreadsheet
		setOptimalWidth(docCursor, true);
		setOptimalWidth(modCursor, true);
		setOptimalWidth(subCursor, true);
	}

	private static void writeHeadings(XSheetCellCursor cursor, String[] strings) {
		try {
			for (int i = 0; i < strings.length; i++) {
				Object cell = cursor.getCellByPosition(i, 0);
				XTextRange textCell = (XTextRange) UnoRuntime.queryInterface(
						XTextRange.class, cell);
				XPropertySet props = (XPropertySet) UnoRuntime.queryInterface(
						XPropertySet.class, cell);
				if (textCell != null) {
					textCell.setString(strings[i]);
				}
				if (props != null) {
					try {
						props.setPropertyValue("CharWeight", new Float(
								FontWeight.BOLD));
					} catch (java.lang.Exception e) {
						e.printStackTrace();
					}
				}
			}
		} catch (IndexOutOfBoundsException e) {
			e.printStackTrace();
		}
	}

	private static void setOptimalWidth(XSheetCellCursor cursor, boolean value) {
		XColumnRowRange xCRR = (XColumnRowRange) UnoRuntime.queryInterface(XColumnRowRange.class,
				cursor);
		if(xCRR!=null) {
			XPropertySet props = (XPropertySet) UnoRuntime.queryInterface(
					XPropertySet.class, xCRR.getColumns());
			if(props!=null)
				try {
					props.setPropertyValue("OptimalWidth", new Boolean(value));
				} catch (java.lang.Exception e) {
					e.printStackTrace();
				}
		}
	}
	
	/**
	 * sets the height of all rows of the sheet.
	 * @param cursor
	 * @param heightmm Height in mm
	 */
	private static void setHeight(XSheetCellCursor cursor, int heightmm) {
		XColumnRowRange xCRR = (XColumnRowRange) UnoRuntime.queryInterface(XColumnRowRange.class,
				cursor);
		if(xCRR!=null) {
			XPropertySet props = (XPropertySet) UnoRuntime.queryInterface(
					XPropertySet.class, xCRR.getRows());
			if(props!=null)
				try {
					props.setPropertyValue("Height", new Integer(heightmm*100));
				} catch (java.lang.Exception e) {
					e.printStackTrace();
				}
		}
	}

	private static void writeDocumentsLine(String docPath, int loc,
			String md5sum) {
		try {
			XCell cell = null;
			XTextRange textCell = null;
			int x = 0;

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					docCursor.getCellByPosition(x++, docCount));
			if (textCell != null) {
				textCell.setString(docPath);
			}

			cell = (XCell) UnoRuntime.queryInterface(XCell.class, docCursor
					.getCellByPosition(x++, docCount));
			if (cell != null) {
				cell.setValue(loc);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					docCursor.getCellByPosition(x++, docCount));
			if (textCell != null) {
				textCell.setString(md5sum);
			}
		
		} catch (IndexOutOfBoundsException e) {
			e.printStackTrace();
		}
		docCount++;
	}

	private static void writeModulesLine(String filePath, String libName,
			String modName, int loc, String md5sum) {
		try {
			XCell cell = null;
			XTextRange textCell = null;
			int x = 0;

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					modCursor.getCellByPosition(x++, modCount));
			if (textCell != null) {
				textCell.setString(filePath);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					modCursor.getCellByPosition(x++, modCount));
			if (textCell != null) {
				textCell.setString(libName);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					modCursor.getCellByPosition(x++, modCount));
			if (textCell != null) {
				textCell.setString(modName);
			}

			cell = (XCell) UnoRuntime.queryInterface(XCell.class, modCursor
					.getCellByPosition(x++, modCount));
			if (cell != null) {
				cell.setValue(loc);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					modCursor.getCellByPosition(x++, modCount));
			if (textCell != null) {
				textCell.setString(md5sum);
			}

		} catch (IndexOutOfBoundsException e) {
			e.printStackTrace();
		}
		modCount++;
	}

	private static void writeFunctionLine(String filePath, String libName,
			String modName, String function, int loc, String md5sum) {
		try {
			XCell cell = null;
			XTextRange textCell = null;
			int x = 0;

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					subCursor.getCellByPosition(x++, subCount));
			if (textCell != null) {
				textCell.setString(filePath);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					subCursor.getCellByPosition(x++, subCount));
			if (textCell != null) {
				textCell.setString(libName);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					subCursor.getCellByPosition(x++, subCount));
			if (textCell != null) {
				textCell.setString(modName);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					subCursor.getCellByPosition(x++, subCount));
			if (textCell != null) {
				textCell.setString(function);
			}

			cell = (XCell) UnoRuntime.queryInterface(XCell.class, subCursor
					.getCellByPosition(x++, subCount));
			if (cell != null) {
				cell.setValue(loc);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					subCursor.getCellByPosition(x++, subCount));
			if (textCell != null) {
				textCell.setString(md5sum);
			}

		} catch (IndexOutOfBoundsException e) {
			e.printStackTrace();
		}
		subCount++;
	}

	private static void writeCodesLine(String md5, int loc, int refsCount,
			String code) {
		try {
			XCell cell = null;
			XTextRange textCell = null;
			int x = 0;

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					codeCursor.getCellByPosition(x++, codeCount));
			if (textCell != null) {
				textCell.setString(md5);
			}

			cell = (XCell) UnoRuntime.queryInterface(XCell.class, codeCursor
					.getCellByPosition(x++, codeCount));
			if (cell != null) {
				cell.setValue(loc);
			}

			cell = (XCell) UnoRuntime.queryInterface(XCell.class, codeCursor
					.getCellByPosition(x++, codeCount));
			if (cell != null) {
				cell.setValue(refsCount);
			}

			textCell = (XTextRange) UnoRuntime.queryInterface(XTextRange.class,
					codeCursor.getCellByPosition(x++, codeCount));
			if (textCell != null) {
				textCell.setString(code);
			}

		} catch (IndexOutOfBoundsException e) {
			e.printStackTrace();
		}
		codeCount++;
	}

	private static int scanDocumentForModules(String filePath, String codeType,
			Object doc) {
		int allLines = 0;

		// get Libraries and Modules
		XStarBasicAccess xSBAccess = (XStarBasicAccess) UnoRuntime
				.queryInterface(XStarBasicAccess.class, doc);
		if (xSBAccess != null) {
			XNameContainer xLibsNC = xSBAccess.getLibraryContainer();
			String[] libs = xLibsNC.getElementNames();
			for (int i = 0; i < libs.length; i++) {
				String libName = libs[i];
				XStarBasicLibraryInfo xSBLInfo = null;
				try {
					xSBLInfo = (XStarBasicLibraryInfo) UnoRuntime
							.queryInterface(XStarBasicLibraryInfo.class,
									xLibsNC.getByName(libName));
				} catch (Exception e) {
					e.printStackTrace();
				}
				if (xSBLInfo != null) {
					XNameContainer xModsNC = xSBLInfo.getModuleContainer();
					String[] mods = xModsNC.getElementNames();
					for (int j = 0; j < mods.length; j++) {
						String modName = mods[j];
						XStarBasicModuleInfo module = null;
						try {
							module = (XStarBasicModuleInfo) UnoRuntime
									.queryInterface(XStarBasicModuleInfo.class,
											xModsNC.getByName(modName));
						} catch (Exception e) {
							e.printStackTrace();
						}
						if (module != null) {
							String code = module.getSource();
							if (codeType.equals("VBA"))
								code = vbaCorrection(code);
							String[] lines = code.split("(\r\n|\n\r|\r|\n)");

							String md5 = getMD5HexRepresentation(code);
							allLines += lines.length;

							writeModulesLine(filePath, libName, modName,
									lines.length, md5);

							// put code to codemap and increase
							// codeReference-Count:
							if (!codeRefs.containsKey(md5)) {
								codeRefs.put(md5, new Integer(1));
								codeMap.put(md5, code);
							} else {
								Integer refsCount = (Integer) codeRefs.get(md5);
								codeRefs.put(md5, new Integer(refsCount
										.intValue() + 1));
							}

							scanFunctionsFromModule(filePath, libName, modName,
									lines);

						}
					}
				}
			}
		}
		return allLines;
	}

	/**
	 * This method removes the Sub/End Sub/Rem statements from the code that OOo
	 * inserts automatically to deactivate VBA-Code.
	 * 
	 * @param code
	 * @return
	 */
	private static String vbaCorrection(String code) {
		String newCode = "";
		String[] lines = code.split("(\r\n|\n\r|\r|\n)");
		for (int i = 0; i < lines.length; i++) {
			String line = lines[i];
			String lineLow = line.toLowerCase();

			if (lineLow.startsWith("sub "))
				continue;
			if (lineLow.startsWith("end sub"))
				continue;
			if (lineLow.startsWith("rem "))
				newCode += line.substring(4) + "\n";
		}
		return newCode;
	}

	private static void scanFunctionsFromModule(String filePath,
			String libName, String modName, String[] codeLines) {
		boolean collect = false;
		ArrayList buf = new ArrayList();
		String function = "";

		for (int i = 0; i < codeLines.length; i++) {
			String line = codeLines[i];

			if (line.toLowerCase().matches(
					"\\s*((private|public)\\s+)*sub\\s+.*")) {
				buf = new ArrayList();
				collect = true;
				function = line;
			}

			if (collect)
				buf.add(line);

			if (line.toLowerCase().matches("\\s*end\\s+sub.*")) {
				collect = false;

				String code = "";
				Iterator flinesIter = buf.iterator();
				while (flinesIter.hasNext()) {
					String fline = (String) flinesIter.next();
					code += fline + "\n";
				}

				String md5 = getMD5HexRepresentation(code);

				writeFunctionLine(filePath, libName, modName, function, buf
						.size(), md5);
			}
		}
	}

	private static String getMD5HexRepresentation(String string) {
		String md5 = "";
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			byte[] md5bytes = md.digest(string.getBytes());
			for (int k = 0; k < md5bytes.length; k++) {
				byte b = md5bytes[k];
				String str = Integer.toHexString(b + 512);
				md5 += str.substring(1);
			}
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return md5;
	}
}

