public class RepositoryAccess {

	protected DocumentIndex index;

	protected Properties properties;
	protected String workingSet = "default";
	protected String srcpath;
	protected String datapath;
	protected String tablesFilename = "tables_v2.json";
	protected String repositoryName;
	public boolean externalMode = false;
	
	public static final String EXTERNAL_MODE = "__external__";

	protected Logger logger;

	public RepositoryAccess(String dir, String working_set) {
		init(dir, working_set);
		properties = new Properties();
	}

	public void setLogger(Logger logger) {
		this.logger = logger;
	}

	public void setOptions(String basedir, String index, String datadir) {
		srcpath = basedir;
		datapath = datadir;
		this.index = new DocumentIndex(index, basedir);
		checkRepo();
	}

	protected void init(String dir, String working_set) {
		File f = new File(dir);
		repositoryName = f.getName();
		workingSet = working_set;

		srcpath = dir + "/src";
		datapath = dir + "/data";
		
		index = new DocumentIndex(srcpath + "/documents.json", srcpath);
		// checkRepo();
		
		externalMode = false;
	}

	public static RepositoryAccess getInstance(String repo, String wset) {
		return new RepositoryAccess(repo, wset);
	}

	public void setProperties(Properties prop) {
		properties = prop;
	}
	
	public String getRepositoryName() {
		return externalMode ? 
				RepositoryAccess.EXTERNAL_MODE : repositoryName;
	}
	
	public String getWorkingSet() {
		return workingSet;
	}

	/**
	 * Test if repository directory structure is present and usable.
	 * If not, attempt to create and initialise.
	 */
	public boolean checkRepo() {
		File datadir = new File(datapath);
		
		File basedir = new File(datadir.getParent());
		if (!basedir.isDirectory()) {
			basedir.mkdir();
		}
		
		if (!datadir.isDirectory() && !datadir.mkdir()) {
			throw new RuntimeException(datadir.getAbsolutePath()
					+ " could not be created");
		}
		File srcrdir = new File(srcpath);
		if (!srcrdir.isDirectory() && !srcrdir.mkdir()) {
			throw new RuntimeException(srcrdir.getAbsolutePath()
					+ " could not be created");
		}

		try {
			index.init();
		} catch (RepositoryException e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}

	/**
	 * return id/names of all documents in the repository
	 * 
	 * @return HashMap<Integer, String> document name and id
	 * @throws RepositoryException
	 */
	public HashMap<Integer, String> getDocuments() throws RepositoryException {
		return index.getDocuments();
	}
	
	/**
	 * Load and return document as a RichDocument object (by id). Document is
	 * opened an parsed, the annotated tables are added.
	 * 
	 * @param id
	 * @return
	 * @throws IOException
	 */
	public RichDocument getDocumentById(int id) throws RepositoryException,
			IOException {
		File f = index.getDocumentById(id);
		if (f == null) {
			return null;
		}
		String filename = f.getAbsolutePath();
		RichDocument d = new RichDocument(filename);
		d.open();
		d.setTableAnnotations(getAnnotatedTables(id));
		return d;
	}

	public String getSourceDirectory() {
		return index.getBasePath().toString();
		// return srcpath;
	}

	/**
	 * Add (or update) a table annotation to/in a document's page
	 * 
	 * @param docid
	 * @param pagen
	 * @param tabledef
	 *            as string in json format
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public void addTableToDocument(int docid, int pagen,
			TableModel tabledef) throws JsonParseException,
			JsonMappingException, IOException, RepositoryException {

		// instantiate doc to validate docid
		RichDocument doc = getDocumentById(docid);

		// Load the set of tables from file
		File file = new File(_getDataDir(docid), tablesFilename);
		DocumentTables tdefs = loadTableDefinitions(file);

		// Create entry page if non-existent
		if (!tdefs.annotationsOnPage.containsKey(pagen)) {
			tdefs.annotationsOnPage.put(pagen, new ArrayList<DocumentTable>());
		}

		// Set up table instance.
		// Try to load by id if set, otherwise create new.
		int table_id = Integer.parseInt(tabledef.id);
		DocumentTable dt = null;
		if (table_id != 0) {
			dt = tdefs.getTable(table_id);
		}
		if (dt == null) {
			dt = DocumentTable.createTable();
			tdefs.annotationsOnPage.get(pagen).add(dt);
		}

		// Set incoming data (pagenumber, cells) to table.
		dt.page = pagen;
		dt.trs = tabledef.trs;

		// Save tables to file.
		saveTableDefinitions(file, tdefs);

		if (properties.getProperty("autosave-export", "") != "") {
			// Update doc before saving auto export.
			doc.setTableAnnotations(tdefs);
			_handleAutoExports(doc);
		}
	}

	/**
	 * Load a DocumentTables data-structure from a JSON file.
	 * 
	 * @param file
	 * @return
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static DocumentTables loadTableDefinitions(File file)
			throws JsonParseException, JsonMappingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		mapper.configure(
				DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		DocumentTables tdefs = new DocumentTables();
		if (file.isFile()) {
			tdefs = mapper.readValue(file, DocumentTables.class);
		}
		return tdefs;
	}

	/**
	 * Save DocumentTables data-structure to disk as JSON.
	 * 
	 * @param file
	 * @param tdefs
	 * @throws JsonGenerationException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static void saveTableDefinitions(File file, DocumentTables tdefs)
			throws JsonGenerationException, JsonMappingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		mapper.configure(
				DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		mapper.writeValue(file, tdefs);
	}

	/**
	 * Load the definitions of annotated tables that are stored for a document.
	 * 
	 * 
	 * @param docid
	 * @return
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public DocumentTables getAnnotatedTables(int docid)
			throws JsonParseException, JsonMappingException, IOException {
		
		File file = new File(_getDataDir(docid), tablesFilename);
		DocumentTables tdefs = loadTableDefinitions(file);
		return tdefs;
	}

	public ArrayList<DocumentTable> getAnnotatedTables(int docid, int pageid)
			throws JsonParseException, JsonMappingException, IOException {
		DocumentTables tdefs = getAnnotatedTables(docid);
		if (tdefs.annotationsOnPage.containsKey(pageid)) {
			return tdefs.annotationsOnPage.get(pageid);
		} else {
			return new ArrayList<DocumentTable>();
		}
	}

	/**
	 * Delete a table-annotation, by table id, page and document-id.
	 * 
	 * @param docid
	 * @param pagen
	 * @param tableid
	 * @throws IOException
	 */
	public void clearTable(int docid, int pagen, int tableid)
			throws IOException {
		File file = new File(_getDataDir(docid), tablesFilename);
		DocumentTables tdefs = loadTableDefinitions(file);
		tdefs.removeTable(pagen, tableid);
		saveTableDefinitions(file, tdefs);
	}

	/**
	 * Clear all annotations for a document.
	 * 
	 * @param docid
	 */
	public void resetAnnotations(int docid) throws IOException {
		File file = new File(_getDataDir(docid), tablesFilename);
		DocumentTables tdefs = new DocumentTables();
		saveTableDefinitions(file, tdefs);
	}

	/**
	 * Clear annotations for a whole document/page.
	 * 
	 * @param docid
	 * @param pagen
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public void clearAnnotations(int docid, int pagen)
			throws JsonParseException, JsonMappingException, IOException {
		File file = new File(_getDataDir(docid), tablesFilename);
		DocumentTables tdefs = loadTableDefinitions(file);
		if (tdefs.annotationsOnPage.containsKey(pagen)) {
			tdefs.annotationsOnPage.remove(pagen);
		}
		saveTableDefinitions(file, tdefs);
	}

	/**
	 * Retrieve data of some model, classifier or preprocessing step regarding a
	 * document. Data is returned in JSON format (no strict types,
	 * interoperability) .
	 * 
	 * @param docid
	 * @param what
	 * @param pagen
	 * @return
	 * @throws JsonGenerationException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public String getDataLayerAsJson(int docid, int pagen, String what)
			throws RepositoryException, IOException {

		ObjectMapper mapper = new ObjectMapper();

		// instantiate doc to validate docid
		RichDocument doc = getDocumentById(docid);

		if (what.equals("chars")) {

			return mapper.writeValueAsString(doc.charactersOnPage.get(pagen));

		} else if (what.equals("words")) {

			ArrayList<ArrayList<BlockInfo>> blockinfo = null;

			WordExtractor we = new WordExtractor();
			we.setCharacters(doc.charactersOnPage.get(pagen));
			we.extractWords();
			return mapper.writeValueAsString(we.getWords());

		} else if (what.equals("table")) {
			// Try to find sparse lines by using
			// the line detection of WordExtractor,
			// then TableBoundaryClassifier
			// (experimental / very basic).

			WordExtractor we = new WordExtractor();
			we.setCharacters(doc.charactersOnPage.get(pagen));
			we.extractWords();
			TableBoundaryClassifier tbc = new TableBoundaryClassifier();
			tbc.setLines(we.getLines());
			tbc.classifyLines();

			ArrayList<BlockInfo> myLines = new ArrayList<BlockInfo>();
			for (ArrayList<BlockInfo> raw_line : tbc.getTableLines()) {
				BlockInfo curline = new BlockInfo();
				for (BlockInfo b : raw_line) {
					curline.addBlock(b);
				}
				myLines.add(curline);
			}

			return mapper.writeValueAsString(myLines);
		} else if (what.equals("import")) {

			// Find/guess structure file
			File file = _findExternalStructFile(doc);
			if (file != null) {
				// Found, import external structure file.
				Importer imp = new Importer();
				DocumentTables tdefs = null;
				try {
					tdefs = imp.getTableDefinitions(file, doc);
				} catch (ParserConfigurationException e) {
					e.printStackTrace();
				} catch (SAXException e) {
					e.printStackTrace();
				}
				if (tdefs.annotationsOnPage.containsKey(pagen)) {
					return mapper.writeValueAsString(tdefs.annotationsOnPage.get(pagen));
				} else {
					// no tables for page
					return "[]";
				}
			} else {
				// no structure file found
				return "[]";
			}
		} else {
			return "";
		}
	}

	/**
	 * Add a document to the repository (by filename) . Throws IOException in
	 * case of invalid file (no pdf) or duplicate file. Copy the file into
	 * repository.
	 * 
	 * @param doc
	 * @throws IOException
	 * @throws RepositoryException
	 */
	public void addDocument(File doc) throws IOException, RepositoryException {
		File importfile = new File(srcpath + '/' + doc.getName());
		FileUtils.copyFile(doc, importfile); // or doc.renameTo(..);

		// Open to validate
		PDDocument document = PDDocument.load(doc.getAbsolutePath());
		document.close();

		// Add to index
		indexDocument(new File(doc.getName()));

	}

	/**
	 * Add a document to index ( but do not copy or move file ) .
	 * 
	 * @param doc
	 * @throws RepositoryException
	 */
	public int indexDocument(File doc) throws RepositoryException {
		return index.add(doc);
	}

	public DocumentIndex getDocumentIndex() {
		return index;
	}

	public Logger getLogger() {
		return logger;
	}

	protected File _findExternalStructFile(RichDocument doc) {
		File source = doc.getSourcePath();
		int pos = source.getName().lastIndexOf(".");
		String trunc_name = pos > 0 ? source.getName().substring(0, pos)
				: source.getName();
		File file;

		file = new File(source.getAbsolutePath() + "-str.xml");
		if (file.isFile()) {
			return file;
		}

		file = new File(source.getParent(), trunc_name + "-str.xml");
		if (file.isFile()) {
			return file;
		}

		file = new File(properties.getProperty("external-datadir", ""),
				doc.getFilename() + "-str.xml");
		if (file.isFile()) {
			return file;
		}

		file = new File(properties.getProperty("external-datadir", ""),
				trunc_name + "-str.xml");
		if (file.isFile()) {
			return file;
		}

		return null;
	}

	/**
	 * A helper to retrieve (and possible create beforehand) the directory for
	 * storing annotation information for a document.
	 * 
	 * @param docid
	 * @return
	 */
	protected File _getDataDir(int docid) {
		File datadir = new File(datapath);
		if (!datadir.isDirectory() && !datadir.mkdir()) {
			throw new RuntimeException(datadir.getAbsolutePath()
					+ " could not be created");
		}
		
		File working_setdir = new File(datapath, workingSet);
		if (!working_setdir.isDirectory() && !working_setdir.mkdir()) {
			throw new RuntimeException(datadir.getAbsolutePath()
					+ " could not be created");
		}

		File dir = new File(working_setdir, String.valueOf(docid));
		if (!dir.isDirectory() && !dir.mkdir()) {
			throw new RuntimeException(dir.getAbsolutePath()
					+ " could not be created");
		}
		return dir;
	}

	public void writeExports(RichDocument doc) throws JsonGenerationException,
			JsonMappingException, ExportException,
			IOException {
		
		File parent = new File(datapath, "exports");
		
		if (!parent.isDirectory()) {
			parent.mkdir();
		}
		writeExports(parent, doc);
	}

	public void writeExports(File parent, RichDocument doc)
			throws ExportException, JsonGenerationException, JsonMappingException, IOException {
		File source = doc.getSourcePath();
		int pos = source.getName().lastIndexOf(".");
		String trunc_name = pos > 0 ? source.getName().substring(0, pos)
				: source.getName();

		// Save structure export.
		StructureExport structure_exp = new StructureExport();
		structure_exp.export(doc, doc.getTableAnnotations());
		structure_exp.toFile(new File(parent, trunc_name + "-str.xml"));

//		// Save JSON.
//		saveTableDefinitions(new File(parent, trunc_name + ".json"),
//				doc.getTableAnnotations());
	}

	/**
	 * Handle "auto Exports" , i.e. saving an export whenever incoming table
	 * data is stored to disk internally.
	 * 
	 * @param doc
	 */
	protected void _handleAutoExports(RichDocument doc) {

		// Determine parent directory.
		File parent = new File(doc.getSourcePath().getParent());
		if (properties.getProperty("autosave-target", "") != "") {
			parent = new File(properties.getProperty("autosave-target"));
		}
		try {
			writeExports(parent, doc);

		} catch (Exception e) {
			System.out.println("Failed to write auto-exports...");
			e.printStackTrace();
		}
	}
} public class RepositoryException extends Exception {
	public RepositoryException(String string) {
		super(string);
	}
	
	public RepositoryException(Exception e) {
		super(e);
	}
} public class TableCell {

	public static String HEADER_HEADER 	= "header";
	public static String HEADER_ROW 	= "rowhead";
	public static String HEADER_COL 	= "colhead";
	public static String HEADER_CAT 	= "stubhead";
	
	/**
	 * Dimensions of cell, as [x1, y1, x2, y2]. 
	 */
	public ArrayList<Float> visgrid;
	
	/**
	 * Allows to tag cell as beeing a header cell.
	 */
	public ArrayList<String> classes;
	
	public int rowspan = 1;
	public int colspan = 1;
	
	/**
	 * Row grid position. Set via javascript frontend.
	 */
	public int startrow;
	
	/**
	 * Col grid position. Set via javascript frontend.
	 */
	public int startcol;
	

	public TableCell() {
		visgrid = new ArrayList<Float>(); // x1, y1, x2, y2
		classes = new ArrayList<String>();
		visgrid.add(-1.0f);
		visgrid.add(-1.0f);
		visgrid.add(-1.0f);
		visgrid.add(-1.0f);
	}

	public TableCell(float x1, float y1, float x2, float y2) {
		visgrid = new ArrayList<Float>();
		visgrid.add(x1);
		visgrid.add(y1);
		visgrid.add(x2);
		visgrid.add(y2);
		classes = new ArrayList<String>();
	}

	public TableCell(ArrayList<Float> visg) {
		this(visg.get(0), visg.get(1), visg.get(2), visg.get(3));
	}

	@JsonIgnore
	public boolean isHeader() {
		if (classes.size() > 0) {
			for (String cls : classes) {
				if (cls == null)
					continue;
				if (cls.equals(HEADER_COL) || cls.equals(HEADER_ROW)
						|| cls.equals(HEADER_CAT) || cls.equals(HEADER_HEADER)) {
					return true;
				}
			}
		}
		return false;
	}

	@JsonIgnore
	public float getDim(String what) {
		if (what.equals("x1")) return visgrid.get(0);
		else if (what.equals("x2")) return visgrid.get(2);
		else if (what.equals("y1")) return visgrid.get(1);
		else if (what.equals("y2")) return visgrid.get(3);
		else throw new RuntimeException("Invalid dimension");
	}
	
	@JsonIgnore
	public void setDim(String what, float val) {
		if (what.equals("x1")) visgrid.set(0,val);
		else if (what.equals("x2")) visgrid.set(2,val);
		else if (what.equals("y1")) visgrid.set(1,val);
		else if (what.equals("y2")) visgrid.set(3,val);
		else throw new RuntimeException("Invalid dimension");
	}
	
	@JsonIgnore
	public boolean hasSize() {
		if ( visgrid.get(2) - visgrid.get(0) < 0.1 || visgrid.get(3) - visgrid.get(1) < 0.1)
			return false;
		return true;
	}
	
	@JsonIgnore
	public String toString() {
		return "TD (" + visgrid + ")" + (hasSize() ? "" : "*");
	}
	
} public class DocumentTable {
	
	/**
	 * Global "max-id".
	 */
	@JsonIgnore
	static public int _tn = 0;
	
	/**
	 * The page the table is on.
	 */
	public int page = 0;
	
	/**
	 * The table's (internal) id.
	 */
	public int tn = 0;
	
	/**
	 * The table's cells, in the form of an array of rows, 
	 * which each is an array of cells.
	 */
	public ArrayList<ArrayList<TableCell>> trs;
	
	public DocumentTable() {
		tn = getNextId();
		trs = new ArrayList<ArrayList<TableCell>>();
	}
	
	public static DocumentTable createTable() {
		DocumentTable tab = new DocumentTable();
		tab.tn = getNextId();
		return tab;
	}
	
	/**
	 * @return A new global id for a table.
	 */
	@JsonIgnore
	public static synchronized int getNextId() {
		_tn++;
		return _tn;	
	}
	

	/**
	 * Create a (canonicalized) grid-representation of the table, where every 
	 * cell in an grid[int_row][int_col] holds a reference to the TableCell cell 
	 * that is at that position. 
	 * If colspan or rowspan is greater one, two or more positions in the grid
	 * can point to the same cell. 
	 */
	@JsonIgnore
	public HashMap<Integer, HashMap<Integer, TableCell>> getAsGrid() {
		HashMap<Integer, HashMap<Integer, TableCell>> grid;
		grid = new HashMap<Integer, HashMap<Integer, TableCell>>(); 
		for ( int i = 0; i < trs.size(); i++) {
			for ( int j = 0; j < trs.get(i).size(); j++) {
				TableCell td = trs.get(i).get(j);
				for (int r = td.startrow; r < td.startrow+td.rowspan; r++) {
					if (!grid.containsKey(r)) {
						grid.put(r, new HashMap<Integer, TableCell>());
					}
					for (int c = td.startcol; c < td.startcol+td.colspan; c++) {
						grid.get(r).put(c, td);
					}
				}
			}
		}
		return grid;
	}
	
	/**
	 * Given that col- and rowspan are set correctly for every cell,
	 * this method calculates the location of each cell in the n,m grid.
	 */
	public void calculateGridLocation() {
		int sizey = trs.size();
		int sizex = 0;
		int i,j = 0;
		
		for ( i=0; i < trs.get(0).size(); i++) {
			sizex+=trs.get(0).get(i).colspan;
		}
		
		int gridhelper[][] = new int[sizey][sizex];
		
		for ( j = 0; j < sizey; j++) {
			Iterator<TableCell> it  = trs.get(j).iterator();
			i=0;
			while (i < sizex && it.hasNext()) {
				// System.out.println("Current:" + (j) + "," + (i) + "=" + gridhelper[j][i]);
				while (gridhelper[j][i] != 0) {
					i++;
				}
				TableCell td = it.next();
				for (int k=1; k<td.rowspan;k++) {
					for(int l=0; l<td.colspan;l++) {
						gridhelper[j+k][i+l] = 1;
					}
				}
				td.startrow = j;
				td.startcol = i;
				i+=td.colspan;
			}
		}
	}
	
	@JsonIgnore
	public int getNRows() {
		return trs.size();
	}
	
	@JsonIgnore
	public int getNCols() {
		if(trs.size()>0)
			return trs.get(0).size();
		else return 0; 
	}
	
	/**
	 * Convert from minimal bounding boxes to a "border-collapse" grid.
	 * 
	 * Work with the left / top boundaries of cell, so that cells "expand". 
	 * 
	 * 
	 */
	public void maximizeCells() {
		HashMap<Integer, HashMap<Integer, TableCell>> grid = getAsGrid();
		int sizey = grid.size();
		int sizex = grid.get(0).size();
		TableCell td;
		HashMap<Integer,ArrayList<Float>> left 		= new HashMap<Integer,ArrayList<Float>>();
		HashMap<Integer,ArrayList<Float>> right 	= new HashMap<Integer,ArrayList<Float>>();
		HashMap<Integer,ArrayList<Float>> top 		= new HashMap<Integer,ArrayList<Float>>();
		HashMap<Integer,ArrayList<Float>> bottom	= new HashMap<Integer,ArrayList<Float>>();
		HashMap<Integer,Float> slices_x	= new HashMap<Integer,Float>();
		HashMap<Integer,Float> slices_y	= new HashMap<Integer,Float>();
		
		for (int j=0; j<sizey; j++) {
			top.put(j, new ArrayList<Float>());
			bottom.put(j, new ArrayList<Float>());
		}
		for ( int i=0; i<sizex; i++) {
			left.put(i, 	new ArrayList<Float>());
			right.put(i, 	new ArrayList<Float>());
		}
		
		//
		// Collect all cell dimensions
		// ! depends on correctly set start col/row !
		//
		// 
		for ( int j = 0 ; j < sizey; j++) {
			for ( int i = 0 ; i < sizex; i++) {
				try {
					td = grid.get(j).get(i);
					if ( td.hasSize() ) {
						if(td.startcol == i) {
							left.get(i).add(td.visgrid.get(0));
						}
						if((td.startcol + td.colspan - 1) == i) {
							right.get(i).add(td.visgrid.get(2));
						}
						
						if(td.startrow == j) {
							top.get(j).add(td.visgrid.get(1));
						}
						
						if((td.startrow + td.rowspan - 1 )== j) {
							bottom.get(j).add(td.visgrid.get(3));
						}
					}					
				} catch (NullPointerException e) {
					System.out.println("Cell missing at " + j + "," + i );
				}
			}
		}
		
		float grid_slice;
		// Determine x - grid lines (slices)
		slices_x.put(0, Collections.min(left.get(0)) - 1.0f);
		for ( int i = 1 ; i < sizex; i++) {
			grid_slice = (Collections.max(right.get(i-1)) + Collections.min(left.get(i)))/2;
			slices_x.put(i, grid_slice);
		}
		slices_x.put(sizex, Collections.max(right.get(sizex-1)) + 1.0f);
		
		// Determine y-grid lines
		slices_y.put(0, Collections.min(top.get(0)) - 1.0f);
		for ( int j = 1 ; j < sizey; j++) {
			grid_slice = Collections.max(bottom.get(j-1));
			if(top.containsKey(j) && top.get(j).size()>0) {
				grid_slice += Collections.min(top.get(j));
				grid_slice /= 2;
			}
			slices_y.put(j, grid_slice);
		}
		if ( bottom.get(sizey-1).size() > 0 ) {
			slices_y.put(sizey, Collections.max(bottom.get(sizey-1))+1.0f);
		}
		
		// Set slice dimensions to cells.
		for ( int j = 0 ; j < sizey; j++) {
			for ( int i = 0 ; i < sizex; i++) {
				
				td = grid.get(j).get(i);
				if(td.hasSize()) {
					if(td.startcol == i) {
						td.visgrid.set(0, slices_x.get(i));
					}
					if((td.startcol + td.colspan - 1) == i) {
						td.visgrid.set(2, slices_x.get(i+1));
					}
					
					if(td.startrow == j) {
						td.visgrid.set(1, slices_y.get(j));
					}
					
					if((td.startrow + td.rowspan - 1 )== j) {
						td.visgrid.set(3, slices_y.get(j+1));
					}
				}
			}
		}
		
	}
} public class DocumentException extends Exception {

	public DocumentException(String string) {
		super(string);
	}

} public class RichDocument {

	protected String sourcePath;
	protected String title = "";
	public ArrayList<ArrayList<CharInfo>> charactersOnPage;
	public ArrayList<PDRectangle> pages_dimensions;
	protected int active_page;
	
	protected DocumentTables tableAnnotations;
	
	public RichDocument(String path) {
		this.sourcePath = path;
		charactersOnPage = new ArrayList<ArrayList<CharInfo>>();
		active_page = 0;
	}
	
	public String getFilename() {
		File f = new File(sourcePath);
		return f.getName();
	}
	
	public File getSourcePath() {
		return new File(sourcePath);
	}
	
	public int getNumberOfPages() {
		return charactersOnPage.size();
	}
	
	public RichDocument setActivePage(int p) throws Exception {
		if (p >= getNumberOfPages()) throw new Exception("page exceeds document length");
		active_page = p;
		return this;
	}
	
	public void open() throws IOException {
		ParsePdf textstripper 	= new ParsePdf(sourcePath);
		try {
			textstripper.parse();
			charactersOnPage = textstripper.getCharacterInfo();
			pages_dimensions = textstripper.getPageDimensions();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * get all characters on active page  within a specified bounding box 
	 * 
	 * @param minx
	 * @param miny
	 * @param maxx
	 * @param maxy
	 * @return
	 * @throws DocumentException 
	 */
	public ArrayList<CharInfo> getCharactersInBoundingBox(float minx, float miny, float maxx, float maxy) throws DocumentException {
		return getCharactersInBoundingBox(active_page, minx, miny, maxx, maxy);
	}

	/**
	 * Get all characters on certain page within a specified bounding box.
	 * Simply loop over all characters and selects those with top/left 
	 * coordinate inside the area. 
	 * 
	 * @param page
	 * @param minx
	 * @param miny
	 * @param maxx
	 * @param maxy
	 * @return
	 * @throws DocumentException 
	 */
	public ArrayList<CharInfo> getCharactersInBoundingBox(int page, float minx, float miny, float maxx, float maxy) throws DocumentException {
		
		// Result will be a list of CharInfo 
		ArrayList<CharInfo> res = new ArrayList<CharInfo>();

		// Work on characters of specified page, make sure that page exists.
		if ( page >= getNumberOfPages()) {
			throw new DocumentException("page " + page + 
					" does not exist ("+getNumberOfPages()+" annotationsOnPage)");
		}
		ArrayList<CharInfo> src = charactersOnPage.get(page);
		
		// Simply loop over all characters and select those with top/left
		// coordinate inside the area.
		for (CharInfo c : src) {
			if (c.y >= miny && (c.y /* + c.h */) < maxy && c.x >= minx
					&& (c.x /* + c.w */) < maxx) {
				res.add(c);
			}
		}

		return res;
	}
	
	public ArrayList<CharInfo> getCharactersInBoundingBox(int page, TableCell td) throws DocumentException {
		return getCharactersInBoundingBox(page, 
				td.visgrid.get(0),
				td.visgrid.get(1),
				td.visgrid.get(2),
				td.visgrid.get(3));
	}
	
	public ArrayList<CharInfo> getCharacters(int page) {
		return charactersOnPage.get(page);
	}
	
	public DocumentTables getTableAnnotations() {
		return tableAnnotations;
	}
	
	public void setTableAnnotations(DocumentTables tdefs) {
		tableAnnotations = tdefs;
	}
	
} public class TableModel {
	public String id;
	public ArrayList<ArrayList<TableCell>> trs;
} public class DocumentTables {
	
	public HashMap<Integer, ArrayList<DocumentTable>> annotationsOnPage;
	
	public DocumentTables() {
		annotationsOnPage = new HashMap<Integer, ArrayList<DocumentTable>>();
	}
	
	/**
	 * Remove a table by specifying page number and table id.
	 * @param pagen
	 * @param tid
	 */
	public void removeTable(int pagen, int tid) {
		if (annotationsOnPage.containsKey(pagen)) {
			for (DocumentTable tab: annotationsOnPage.get(pagen)) {
				if (tab.tn == tid) {
					annotationsOnPage.get(pagen).remove(tab);
					break;
				}
			}
		}
	}
	
	/**
	 * Add a DocumentTable. Table will be added to the page that is specified
	 * with the table.
	 * @param tab
	 */
	public void addTable(DocumentTable tab) {
		addTable(tab.page, tab);
	}
	
	/**
	 * Add a DocumentTable, to a specific page number. That page number will
	 * also be set to the table.
	 * @param pagen
	 * @param tab
	 */
	public void addTable(int pagen, DocumentTable tab) {
		if (!annotationsOnPage.containsKey(pagen)) {
			annotationsOnPage.put(pagen, new ArrayList<DocumentTable>());
		}
		tab.page = pagen;
		annotationsOnPage.get(pagen).add(tab);
	}
	
	/**
	 * Get a table (table definitions) by its numeric id. 
	 * @param tid
	 * @return
	 */
	@JsonIgnore
	public DocumentTable getTable(int tid) {
		for(int pagen : annotationsOnPage.keySet()) {
			for (DocumentTable tab: annotationsOnPage.get(pagen)) {
				if (tab.tn == tid) {
					return tab;
				}
			}
		}
		return null;
	}

	/**
	 * Get all tables on a page. 
	 * @param tid
	 * @return
	 */
	@JsonIgnore
	public ArrayList<DocumentTable> getTables(int page) {
		if (annotationsOnPage.containsKey(page)) {
			return annotationsOnPage.get(page);
		} else {
			return new ArrayList<DocumentTable>();
		}
	}

	@JsonIgnore
	public ArrayList<DocumentTable> getTables() {
		ArrayList<DocumentTable> all = new ArrayList<DocumentTable>();
		for ( Map.Entry <Integer, ArrayList<DocumentTable>> pageEntry : annotationsOnPage.entrySet()) {
			all.addAll(pageEntry.getValue());
		}
		return all;
	}
} public class DOMExport extends Export {
	
	protected Document dom;
	protected String format;
	
	public DOMExport() {
		super();
		dom = null;
		format = "xml";
	}
	
	
	public void export() throws ExportException {
		dom = export(doc, doc.getTableAnnotations());
	}
	
	/**
	 * export 
	 * uses subclass' buildDom() method to prepare the actual DOM
	 * @param doc
	 * @param tdef
	 * @return
	 * @throws ParserConfigurationException
	 */
	public Document export(RichDocument doc, DocumentTables tdef) throws ExportException {
		try {
			initExportDom();
			buildDom(doc, tdef);
		} catch (DocumentException e) {
			e.printStackTrace();
			throw new ExportException(e.getMessage());
		} catch (ParserConfigurationException e) {
			throw new ExportException(e.getMessage());
		}
		return dom;
	}
	
	public String getMimeType() {
		return "text/" + format;
	}
	
	public String getExportFilename(String docname) {
		return docname + "." + getFormat();
	}
	
	public String getFormat() {
		return format;
	}
	
	/**
	 * set up dom transformer
	 *  
	 * @return
	 * @throws ExportException 
	 * @throws TransformerConfigurationException
	 * @throws TransformerFactoryConfigurationError
	 */
	public Transformer getDefaultTransformer() throws ExportException {
		Transformer tr = null;
		try {
			tr = TransformerFactory.newInstance().newTransformer();
		} catch (TransformerConfigurationException e) {
			throw new ExportException(e.getMessage());
		} catch (TransformerFactoryConfigurationError e) {
			throw new ExportException(e.getMessage());
		}
		tr.setOutputProperty(OutputKeys.INDENT, "yes");
		tr.setOutputProperty(OutputKeys.METHOD, format);
		tr.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tr.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		return tr;
	}
	
	/**
	 * dom transformed as string
	 * @throws ExportException 
	 */
	public String toString() {
		Transformer tr;
		StringWriter writer = new StringWriter();
		try {
			tr = getDefaultTransformer();
			tr.transform(new DOMSource(dom), new StreamResult(writer));
		} catch (ExportException e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		}
		
		return writer.getBuffer().toString();
	}
	
	/**
	 * dom transformed to file
	 * @param outfile
	 * @throws TransformerFactoryConfigurationError
	 * @throws FileNotFoundException
	 * @throws TransformerException
	 */
	public void toFile(File outfile) throws  FileNotFoundException, ExportException {
		try {
			Transformer tr = getDefaultTransformer();
			tr.transform(new DOMSource(dom), new StreamResult(new FileOutputStream(outfile)));
		} catch (TransformerFactoryConfigurationError e) {
			throw new ExportException(e.getMessage());
		} catch (TransformerException e) {
			throw new ExportException(e.getMessage());
		}
	}
	
	/**
	 * initialize dom 
	 * @throws ParserConfigurationException
	 */
	protected void initExportDom() throws ParserConfigurationException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		// dbf.setNamespaceAware(false);
		DocumentBuilder db = dbf.newDocumentBuilder();
		dom = db.newDocument();
	}
	
	/**
	 * override .. 
	 * @throws ParserConfigurationException 
	 * @throws DocumentException 
	 */
	protected void buildDom(RichDocument doc, DocumentTables tdef) throws ParserConfigurationException, DocumentException { }
} public class Export {
	String format = "plain";
	RichDocument doc;
	
	public String getMimeType() {
		return "text/" + format;
	}
	
	public void setDocument(RichDocument d) {
		doc = d;
	}
	
	public void export() throws ExportException {
		
	}
	
	public String getExportFilename(String docname) {
		return docname + "." + getFormat();
	}
	
	public String getFormat() {
		return format;
	}
	
	public String toString() {
		return "";
	}
	
	public void toFile(File outfile) throws  FileNotFoundException, Exception {
		// new FileOutputStream(outfile);
	}
	
	public static Export factory(String export_name) {
		Export exp = null;
		if (export_name.toLowerCase().equals("html")) 
			exp = new HtmlExport();
		else if (export_name.toLowerCase().equals("structure"))
			exp = new StructureExport();
		else if (export_name.toLowerCase().equals("region"))
			exp = new RegionExport();
		else if (export_name.toLowerCase().equals("logical")
				|| export_name.toLowerCase().equals("functional"))
			exp = new LogicalStructureExport();
		else if (export_name.toLowerCase().equals("csv"))
			exp = new CsvExport();
		
		return exp;
	}
} public class ExportException extends Exception {
	public ExportException(String string) {
		super(string);
	}
	
	public ExportException(Exception e) {
		super(e);
	}
} public class HtmlExport extends DOMExport{
	
	public HtmlExport() {
		super();
		format = "html";
	}
	
	public void buildDom(RichDocument doc, DocumentTables tdef) throws ParserConfigurationException, DocumentException {
		
		Element root = dom.createElement("html");
		dom.appendChild(root);
		
		Element body = dom.createElement("body");
		root.appendChild(body);
		
		Element docname_h1 = dom.createElement("h1");
		docname_h1.appendChild(dom.createTextNode(doc.getFilename()));
		body.appendChild(docname_h1);
		
		int table_id = 0;
		for ( DocumentTable t : tdef.getTables()) {
			Element tab 				= dom.createElement("table");
			tab.setAttribute("id", 		Integer.toString(table_id));
			tab.setAttribute("data-page", 	Integer.toString(t.page + 1));		// add one
			tab.setAttribute("border", 	"1");
			
			BlockInfo regionblock = new BlockInfo();
			
			for ( int i = 0; i < t.trs.size(); i++) {
				
				Element tr = dom.createElement("tr");
				tab.appendChild(tr);
				
				for ( int j = 0; j < t.trs.get(i).size(); j++) {
					
					TableCell td = t.trs.get(i).get(j);
					
					ArrayList<CharInfo> in_chars = doc.getCharactersInBoundingBox(
							t.page, td.visgrid.get(0), td.visgrid.get(1), 
							td.visgrid.get(2), td.visgrid.get(3));
					BlockInfo bi = new BlockInfo(in_chars, "");
					
					Element cell = dom.createElement("td");
					cell.setAttribute("rowspan", Integer.toString(td.rowspan));
					cell.setAttribute("colspan", Integer.toString(td.colspan));

					// the minimal bounding box spanned by character content
					cell.setAttribute("data-bbox", "["+
							"minx=" +  Float.toString(bi.x) + "," +
							"maxx=" +  Float.toString(bi.x + bi.w) + "," +
							"miny=" +  Float.toString(bi.y) + "," +
							"maxy=" +  Float.toString(bi.y + bi.h) + "]");
					
					
					// css-style information 
					cell.setAttribute("style","width:"+(td.visgrid.get(2)-td.visgrid.get(0))
							+"px;height:"+(td.visgrid.get(3)-td.visgrid.get(1))+"px");
					
					// the (larger) area that was drawn to mark the cell
					cell.setAttribute("data-cellbox", "[minx="+td.visgrid.get(0)
							+", maxx="+td.visgrid.get(2)
							+", miny="+td.visgrid.get(1)
							+", maxy="+td.visgrid.get(3)+"]");
							
					// Annotated class of cell
					// TODO: support multiple classes per cell. 
					if ( td.classes.size() > 0) {
						cell.setAttribute("data-class", td.classes.get(0));
						cell.setAttribute("class", td.classes.get(0));
					}

					// textual content
					cell.appendChild(dom.createTextNode(bi.getCharactersAsString()));
					
					tr.appendChild(cell);
					regionblock.addBlock(bi);
				}
			}
			
			// bounding box of table region
			tab.setAttribute("data-bbox", "["+
					"minx=" +  Float.toString(regionblock.x) + "," +
					"maxx=" +  Float.toString(regionblock.x + regionblock.w) + "," +
					"miny=" +  Float.toString(regionblock.y) + "," +
					"maxy=" +  Float.toString(regionblock.y + regionblock.h) + "]");
			
			body.appendChild(tab);
			
			table_id++;
		}
	}
	
} public class LogicalStructureExport extends DOMExport {
	
	Element transformTable(DocumentTable t, RichDocument src) throws DocumentException {
		int i, row, col;
		Element table = dom.createElement("table");
		
		// Map cells to a grid
		HashMap<Integer, HashMap<Integer, TableCell>> grid = t.getAsGrid();
		
		// Store logic table.
		ArrayList<ArrayList<HashSet<TableCell>>> logictable 
			= new ArrayList<ArrayList<HashSet<TableCell>>>();
		
		// Loop over cells in grid.
		for ( row = 0; row < t.trs.size(); row++) {
			for ( col = 0; col < t.trs.get(row).size(); col++) {
				
				// Current cell.
				TableCell td = t.trs.get(row).get(col);
				
				// Sets to store the current cell's access cells in.
				HashSet<TableCell> colset = new HashSet<TableCell>();
				HashSet<TableCell> rowset = new HashSet<TableCell>();
				
				// Is cell a header cell itself ? 
				// - if so, ignore.
				if (td.isHeader()) continue;
				
				// Examine other cells in the same row.
				// If there are header-cells, add them as row-headers.
				for(i=0; i < t.getNCols(); i++) {
					if(grid.get(td.startrow).get(i).isHeader()) {
						rowset.add(grid.get(td.startrow).get(i));
					}
				}
				
				// Examine other cells in the same column.
				// If there are header-cells, add them as row-headers.
				for(i=0; i < t.getNRows(); i++) {
					if(grid.get(i).get(td.startcol).isHeader()) {
						colset.add(grid.get(i).get(td.startcol));
					}
				}

				// Add the cell and it's accesspath to the collection of cells
				// ((rowheaders), (colheaders), (datacells))
				ArrayList<HashSet<TableCell>> accesspath = new ArrayList<HashSet<TableCell>>();
				accesspath.add(rowset);
				accesspath.add(colset);
				HashSet<TableCell> dataset = new HashSet<TableCell>();
				dataset.add(td);
				accesspath.add(dataset);
				
				logictable.add(accesspath);
			}
		}
		
		// build xml representation
		for(ArrayList<HashSet<TableCell>> access : logictable) {
			Element e_value = dom.createElement("value");
			table.appendChild(e_value);
			
			// prepare 3 sets of values (2 dim access, 1 datacell)
			ArrayList<ArrayList<String>> dimdata = new ArrayList<ArrayList<String>>();
			for( i=0; i<3; i++) {
				dimdata.add(new ArrayList<String>());
				for (TableCell tdout : access.get(i)) {
					BlockInfo bi = new BlockInfo(src.getCharactersInBoundingBox(t.page, tdout), "");
					dimdata.get(i).add(bi.getCharactersAsString());
				}
			}
			
			Element dimx = dom.createElement("dimension");
			dimx.appendChild(dom.createTextNode(str_join(dimdata.get(0),".")));
			e_value.appendChild(dimx);
			Element dimy = dom.createElement("dimension");
			dimy.appendChild(dom.createTextNode(str_join(dimdata.get(1),".")));
			e_value.appendChild(dimy);
			Element datval = dom.createElement("data");
			datval.appendChild(dom.createTextNode(str_join(dimdata.get(2),",")));
			e_value.appendChild(datval);
			
			// System.out.println(rowaccess + "/" + colaccess + "/" + contents);
		}
		
		return table;
	}
	
	protected void buildDom(RichDocument doc, DocumentTables tdef) throws ParserConfigurationException, DocumentException {
		
		Element document = dom.createElement("document");
		document.setAttribute("filename", doc.getFilename());
		dom.appendChild(document);
		
		int table_id = 0;
		for ( DocumentTable t : tdef.getTables()) {
			Element table = transformTable(t,  doc);
			document.appendChild(table);
			table.setAttribute("tid", Integer.toString(table_id));
			table_id++;
		}
	}
	
	protected String str_join(Collection s, String delimiter) {
		// from http://stackoverflow.com/questions/1515437/java-function-for-arrays-like-phps-join
	    StringBuffer buffer = new StringBuffer();
	    Iterator iter = s.iterator();
	    while (iter.hasNext()) {
	        buffer.append(iter.next());
	        if (iter.hasNext()) {
	            buffer.append(delimiter);
	        }
	    }
	    return buffer.toString();
	}
} public class RegionExport extends DOMExport {
	
	public String getExportFilename(String docname) {
		return docname + "-reg." + getFormat();
	}
	
	public void buildDom(RichDocument doc, DocumentTables tdef) throws ParserConfigurationException, DocumentException {
		
		Element document = dom.createElement("document");
		document.setAttribute("filename", doc.getFilename());
		dom.appendChild(document);
		
		//
		// TODO : add the following attr's to root element (document):
		//
		//				xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
		//				xsi:noNamespaceSchemaLocation="competition-entry-region-model.xsd"
		
		
		int table_id = 0;
		for ( DocumentTable t : tdef.getTables()) {
			Element tab = dom.createElement("table");
			tab.setAttribute("id", Integer.toString(table_id));
			Element region = dom.createElement("region");
			region.setAttribute("id", Integer.toString(0));
			region.setAttribute("page", Integer.toString(t.page));
			tab.appendChild(region);
			document.appendChild(tab);
			
			ArrayList<CharInfo> region_chars = new ArrayList<CharInfo>();
			for ( int i = 0; i < t.trs.size(); i++) {
				for ( int j = 0; j < t.trs.get(i).size(); j++) {
					// initialise td and get its contents (characters) 
					TableCell td = t.trs.get(i).get(j);
					region_chars.addAll(doc.getCharactersInBoundingBox(
							t.page, td.visgrid.get(0), td.visgrid.get(1), 
							td.visgrid.get(2), td.visgrid.get(3)));
				}
			}
			
			BlockInfo region_block = new BlockInfo(region_chars, "");
			Element bbox = dom.createElement("bounding-box");
			bbox.setAttribute("x1", Float.toString(region_block.x));
			bbox.setAttribute("x2", Float.toString(region_block.x + region_block.w));
			bbox.setAttribute("y1", Float.toString(region_block.y));
			bbox.setAttribute("y2", Float.toString(region_block.y + region_block.h));
			region.appendChild(bbox);
			
			table_id++;
		}
	}
} public class StructureExport extends DOMExport {
	
	public String getExportFilename(String docname) {
		return docname + "-str." + getFormat();
	}
	
	protected void buildDom(RichDocument doc, DocumentTables tdef) throws ParserConfigurationException, DocumentException {
		
		Element document = dom.createElement("document");
		document.setAttribute("filename", doc.getFilename());
		dom.appendChild(document);
		
		int table_id = 0;
		for ( DocumentTable t : tdef.getTables()) {
			Element tab = dom.createElement("table");
			tab.setAttribute("id", Integer.toString(table_id));
			Element region = dom.createElement("region");
			region.setAttribute("col-increment", Integer.toString(0));
			region.setAttribute("row-increment", Integer.toString(0));
			region.setAttribute("id", Integer.toString(0)); //no regions, so region id is 0 
			region.setAttribute("page", Integer.toString(t.page + 1)); // add 1 to account for 1-based counting in export
			tab.appendChild(region);
			document.appendChild(tab);
			
			float page_height = doc.pages_dimensions.get(t.page).getHeight();
			int cell_id = 0;
			for ( int i = 0; i < t.trs.size(); i++) {
				for ( int j = 0; j < t.trs.get(i).size(); j++) {
					
					TableCell td = t.trs.get(i).get(j);
					ArrayList<CharInfo> in_chars = doc.getCharactersInBoundingBox(
							t.page, td.visgrid.get(0), td.visgrid.get(1), 
							td.visgrid.get(2), td.visgrid.get(3));
					BlockInfo bi = new BlockInfo(in_chars, "");
					
					Element cell = dom.createElement("cell");
					cell.setAttribute("id", Integer.toString(cell_id));
					cell.setAttribute("start-col", Integer.toString(td.startcol));
					cell.setAttribute("start-row", Integer.toString(td.startrow));
					
					Element bbox = dom.createElement("bounding-box");
					bbox.setAttribute("x1", Integer.toString(Math.round(bi.x)));
					bbox.setAttribute("x2", Integer.toString(Math.round(bi.x + bi.w)));
					
					bbox.setAttribute("y1", Integer.toString(Math.round(page_height - (bi.y + bi.h))));
					bbox.setAttribute("y2", Integer.toString(Math.round(page_height - bi.y)));
					
					Element content = dom.createElement("content");
					content.appendChild(dom.createTextNode(bi.getCharactersAsString()));
					
					cell.appendChild(bbox);
					cell.appendChild(content);
					region.appendChild(cell);
					
					cell_id++;
				}
			}
			table_id++;
		}
	}
} public class TableDetectionFromRegion {

	public DocumentTable extract(RichDocument doc, int page, ArrayList<Float> region) throws DocumentException {
		
		DocumentTable tab = new DocumentTable();
		tab.page = page;
		
		ArrayList<CharInfo> chars = doc.getCharactersInBoundingBox(page,
				region.get(0), region.get(1), region.get(2), region.get(3));
		
		// cluster characters to words and lines
		WordExtractor we = new WordExtractor();
		we.setCharacters(chars);
		we.extractWords();
		ArrayList<ArrayList<BlockInfo>> lines = we.getLines();
		
		// from words in lines, determine a table grid 
		for(ArrayList<BlockInfo> tds : lines) {
			ArrayList<TableCell> tr = new ArrayList<TableCell>();
			BlockInfo bi = tds.get(0);
			tr.add(new TableCell(bi.x, bi.y, bi.x + bi.w, bi.y + bi.h));
			tab.trs.add(tr);
		}
		
		// 
		// stretch tds vertically ( = convert from inner boundingboxes to 
		// 	to grid ) 
		// 
		float lasty = 0;
		for ( int j = 0; j < tab.trs.size()-1; j++) {
			TableCell td_cur 	= tab.trs.get(j).get(0);
			TableCell td_next 	= tab.trs.get(j+1).get(0);
			td_cur.visgrid.set(3, td_next.visgrid.get(1));
		}
		
/*
 * we should : 
 * 
 * get our y values like this : 
 * beginblock, endblock, beginblock, endblock
 * take the middle between each endblock-beginblock
 * add some slack at bottom + top ,but maximum to the boundaries of the original
 * region - or, rather maybe exactly from / up to the original region ) 
 * 
 * how to find "sub-rows" that need to be merged ? 
 * 
 * how to do splitting on the horizontal axis ? 
 * 		=> list all x values that form spaces between words over all TRs
 * 		=> take those, that ... have  certain sizes,
 * 							... occur on many / all rows ! 
 * 
 * ( algorithm that works from parts upwards, and merges parts to one cell if
 * that seems reasonable ..)
 * 		
 */
		
		return tab;
	}
}
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import at.tugraz.kti.pdftable.data.RepositoryAccess;
import at.tugraz.kti.pdftable.data.RepositoryException;
import at.tugraz.kti.pdftable.document.TableCell;
import at.tugraz.kti.pdftable.document.DocumentTable;
import at.tugraz.kti.pdftable.document.DocumentException;
import at.tugraz.kti.pdftable.document.RichDocument;
import at.tugraz.kti.pdftable.document.TableModel;
import at.tugraz.kti.pdftable.document.DocumentTables;
import at.tugraz.kti.pdftable.document.export.DOMExport;
import at.tugraz.kti.pdftable.document.export.Export;
import at.tugraz.kti.pdftable.document.export.ExportException;
import at.tugraz.kti.pdftable.document.export.HtmlExport;
import at.tugraz.kti.pdftable.document.export.LogicalStructureExport;
import at.tugraz.kti.pdftable.document.export.RegionExport;
import at.tugraz.kti.pdftable.document.export.StructureExport;
import at.tugraz.kti.pdftable.extract.TableDetectionFromRegion;
package at.tugraz.kti.pdftable.frontend;





/**
 * 
 * @author "matthias frey"
 * 
 *         Defines REST resources endpoints.
 * 
 */
@Path("/")
@Produces("text/plain")
public class Resource {

	App app;

	public Resource() throws SecurityException, IOException {
		app = App.getInstance();
	}

	@GET
	@Path("/settings")
	public Response getSettings(@Context HttpServletRequest req)
			throws JsonGenerationException, JsonMappingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		HttpSession ses = req.getSession();
		HashMap<String, String> output = new HashMap<String, String>();
		output.put("user_id", (String) ses.getAttribute("user_id"));
		output.put("repo", app.getRepositoryAccess(req).getRepositoryName());
		return Response.ok(mapper.writeValueAsString(output),
				MediaType.APPLICATION_JSON).build();
	}

	@POST
	@Path("/settings")
	public Response setSettings(@Context HttpServletRequest req)
			throws JsonGenerationException, JsonMappingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		HttpSession ses = req.getSession();
		if (req.getParameterMap().containsKey("user_id")) {
			ses.setAttribute("user_id", req.getParameter("user_id"));
		}
		if (req.getParameterMap().containsKey("repo")) {
			if ( req.getParameter("repo") != RepositoryAccess.EXTERNAL_MODE) {