Java tutorial
/*- * Copyright 2009 Diamond Light Source Ltd., Science and Technology * Facilities Council Daresbury Laboratory * * This file is part of GDA. * * GDA is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License version 3 as published by the Free * Software Foundation. * * GDA 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 General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with GDA. If not, see <http://www.gnu.org/licenses/>. */ package gda.function.lookup; import gda.configuration.properties.LocalProperties; import gda.device.DeviceException; import gda.factory.Configurable; import gda.factory.Findable; import gda.factory.Localizable; import gda.function.Lookup; import gda.observable.IObserver; import gda.observable.ObservableComponent; import gda.util.QuantityFactory; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.StringTokenizer; import org.apache.commons.collections.map.MultiValueMap; import org.jscience.physics.quantities.Quantity; import org.jscience.physics.units.Unit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class provides a generic lookup function for looking up the value for a scannable (scannable name as column * heading)that corresponding to a given key (such as row name). It reads a text file which contains columnar data in * the following format: * <ul> * <li>Comments: Any line beginning with # is ignored</li> * <li>Column Heading: the name of scannable objects, both Java Scannable and Jython PseudoDevice objects are supported, * starting with a Marker string constant "ScannableNames" (the default) or Spring configured property "columnHead" used * as key</li> * <li>Unit String: the physical units used by each of the scannables, starting with a Marker string constant * "ScannableUnits" (the default) or Spring configure property "columnUnit"</li> * <li>Lookup Values: Multiple rows and columns of data with NO Marker, instead, the first column of data (i.e. the * left-most scannable's values) are used as key by default for the lookup on specific row.</li> * </ul> * it uses a MultiValuedMap object to store the lookup table. */ public class LookupTable implements Findable, Configurable, Lookup, Localizable { /** * the logger instance */ private static final Logger logger = LoggerFactory.getLogger(LookupTable.class); /** * the name of this object */ private String name; /** * total number of rows in the lookup table */ private int numberOfRows; /** * the number of decimal places used for the scannable objects */ private ArrayList<Object> keys; /** * the filename of the lookup table - ASCII file */ private String filename; private String dirLUT = null; /** * Column head holds the name of the column header - previously this was hard coded to ScannableNames - so * defaulting to to 'ScannableNames' for backward compatability */ private String columnHead = "ScannableNames"; /** * Column head holds the name of the column units - previously this was hard coded to ScannableUnits - so defaulting * to to 'ScannableUnits' for backward compatability */ private String columnUnit = "ScannableUnits"; /** * @return Returns the columnHead. */ public String getColumnHead() { return columnHead; } /** * @param columnHead * The columnHead to set. */ public void setColumnHead(String columnHead) { this.columnHead = columnHead; } /** * @return Returns the columnUnit. */ public String getColumnUnit() { return columnUnit; } /** * @param columnUnit * The columnUnit to set. */ public void setColumnUnit(String columnUnit) { this.columnUnit = columnUnit; } public String getDirLUT() { return dirLUT; } public void setDirLUT(String dirLUT) { this.dirLUT = dirLUT; } private MultiValueMap lookupMap = new MultiValueMap(); private boolean configured = false; private ObservableComponent observableComponent = new ObservableComponent(); private boolean local = false; /** * default constructor */ public LookupTable() { } @Override public void configure() { logger.info("{} configuring lookup table. This will take a while, please wait ......", getName()); // logger.debug("LookupTable configure called"); if (!configured) { String filePath; if (dirLUT == null) { String gda_config = LocalProperties.get(LocalProperties.GDA_CONFIG); String lookupTableFolder = LocalProperties.get("gda.function.lookupTable.dir", gda_config + File.separator + "lookupTables"); filePath = lookupTableFolder + File.separator + filename; } else { filePath = dirLUT + File.separator + filename; } readTheFile(filePath); configured = true; } } private void checkConfigured() throws DeviceException { if (!configured) throw new DeviceException("LookupTable '" + getName() + "' is not configured"); } @Override public void reload() { configured = false; configure(); } /** * Reads the lookup table file and put them into a multi-Valued Map for looking up value for the specified energy * and scannable name. * * @param filePath */ private void readTheFile(String filePath) { lookupMap.clear(); BufferedReader br = null; String nextLine; String[] names = null; String[] unitStrings = null; int[] decimalPlaces = null; ArrayList<String> lines = new ArrayList<String>(); try { // Find out lookup table folder logger.info("loading the look up table file {} ", filePath); br = new BufferedReader(new FileReader(filePath)); while (((nextLine = br.readLine()) != null) && (nextLine.length() > 0)) { if (nextLine.startsWith(getColumnHead())) { //logger.debug(nextLine); // NB This regex means one or more comma space or tab // This split will include the word "ScannableNames" as one of the names names = nextLine.split("[, \t][, \t]*"); } else if (nextLine.startsWith(getColumnUnit())) { //logger.debug(nextLine); // NB This regex means one or more comma space or tab // This split will include the word "ScannableUnits" as one of the unitStrings unitStrings = nextLine.split("[, \t][, \t]*"); } else if (!nextLine.startsWith("#")) lines.add(nextLine); } } catch (FileNotFoundException fnfe) { throw new IllegalArgumentException("LookupTable could not open file " + filePath, fnfe); } catch (IOException ioe) { throw new RuntimeException("LookupTable IOException ", ioe); } finally { try { if (br != null) { br.close(); } } catch (IOException e) { logger.error("IOException on loading look up table", e); throw new RuntimeException("IOException on loading look up table", e); } } numberOfRows = lines.size(); logger.debug("the file containes " + numberOfRows + " lines"); int nColumns = new StringTokenizer(lines.get(0), ", \t").countTokens(); logger.debug("each line contains " + nColumns + " numbers"); keys = new ArrayList<Object>(); if (names != null) { for (int i = 0; i < nColumns; i++) { lookupMap.put(names[0], names[i + 1]); } keys.add(names[0]); } if (unitStrings != null) { for (int i = 0; i < nColumns; i++) { lookupMap.put(unitStrings[0], QuantityFactory.createUnitFromString(unitStrings[i + 1])); } keys.add(unitStrings[0]); } decimalPlaces = calculateDecimalPlaces(lines.get(0)); if (decimalPlaces != null) { for (int i = 0; i < nColumns; i++) { lookupMap.put("DecimalPlaces", decimalPlaces[i]); } keys.add("DecimalPlaces"); } double[] thisLine; int i; int j; for (i = 0; i < numberOfRows; i++) { nextLine = lines.get(i); thisLine = stringToDoubleArray(nextLine); for (j = 0; j < thisLine.length; j++) lookupMap.put(String.format("%.3f", thisLine[0]), thisLine[j]); keys.add(String.format("%.3f", thisLine[0])); } } @Override @SuppressWarnings("unchecked") public ArrayList<String> getScannableNames() throws DeviceException { checkConfigured(); synchronized (lookupMap) { return (ArrayList<String>) lookupMap.getCollection(getColumnHead()); } } /** * the energy value needs to be Double parseable, otherwise the method * @throws DeviceException */ @Override @SuppressWarnings("unchecked") public double lookupValue(Object energy, String scannableName) throws DeviceException { checkConfigured(); double result; synchronized (lookupMap) { int index = indexOfScannable(getScannableNames(), scannableName); double energyDouble = Double.parseDouble(energy.toString()); String value = String.format("%.3f", energyDouble); logger.debug("energy is : {}", value); ArrayList<Double> list = (ArrayList<Double>) lookupMap.getCollection(value); if (list == null) { logger.error("{}: list is null", getName()); throw new IllegalStateException("Cannot find value for " + scannableName + " at energy " + energy); } result = list.get(index); return result; } } /** * @param scannableName * @return Unit * @throws DeviceException */ @SuppressWarnings("unchecked") public Unit<? extends Quantity> lookupUnit(String scannableName) throws DeviceException { checkConfigured(); synchronized (lookupMap) { return ((ArrayList<Unit<? extends Quantity>>) lookupMap.getCollection(getColumnUnit())) .get(indexOfScannable(getScannableNames(), scannableName)); } } @Override @SuppressWarnings("unchecked") public synchronized String lookupUnitString(String scannableName) throws DeviceException { checkConfigured(); synchronized (lookupMap) { Unit<? extends Quantity> unit = ((ArrayList<Unit<? extends Quantity>>) lookupMap .getCollection(getColumnUnit())).get(indexOfScannable(getScannableNames(), scannableName)); return unit.toString(); } } @Override @SuppressWarnings("unchecked") public synchronized int lookupDecimalPlaces(String scannableName) throws DeviceException { checkConfigured(); synchronized (lookupMap) { return ((ArrayList<Integer>) lookupMap.getCollection("DecimalPlaces")) .get(indexOfScannable(getScannableNames(), scannableName)); } } /** * returns or find the index of a scannable name in an Arraylist of Scannable names * * @param scannableNames * @param scannableName * @return index */ private int indexOfScannable(ArrayList<String> scannableNames, String scannableName) { return scannableNames.indexOf(scannableName); } /** * Takes a string of comma, space or tab separated values and parses it into an array of doubles * * @param string * the input string * @return an array of doubles found in the string */ private double[] stringToDoubleArray(String string) { double[] values; StringTokenizer strtok = new StringTokenizer(string, ", \t"); values = new double[strtok.countTokens()]; int i = 0; while (strtok.hasMoreTokens()) { values[i] = Double.valueOf(strtok.nextToken()).doubleValue(); i++; } return values; } /** * @param string * @return an array of token positions */ private int[] calculateDecimalPlaces(String string) { int[] values; String nextToken; int index; StringTokenizer strtok = new StringTokenizer(string, ", \t"); values = new int[strtok.countTokens()]; int i = 0; while (strtok.hasMoreTokens()) { nextToken = strtok.nextToken(); index = nextToken.indexOf('.'); if (index == -1) values[i] = 0; else values[i] = nextToken.length() - index - 1; i++; } return values; } @Override public int getNumberOfRows() { return numberOfRows; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public void setFilename(String filename) { this.filename = filename; } @Override public String getFilename() { return filename; } private MultiValueMap getLookupMap() { return this.lookupMap; } /** * @return array of the values in the first column, considering that the first three rows are ScannableNames, * ScannableUnits, and DecimalPlaces * @throws DeviceException */ @Override public double[] getLookupKeys() throws DeviceException { checkConfigured(); ArrayList<Object> actualKeys = getKeys(); final int firstRows = 3;// ScannableNames, ScannableUnits, DecimalPlaces int totalKeys = actualKeys.size(); if (totalKeys > firstRows) { double[] keys = new double[totalKeys - firstRows]; for (int count = firstRows; count < actualKeys.size(); count++) { // While reading the file there is a mandate that the first column must be a double value - so // additional checks aren't required here keys[count - firstRows] = Double.parseDouble((String) actualKeys.get(count)); } return keys; } return null; } // below is testing code used in this class internally only /** * configures the object, used by this class's main test method only * * @param filename */ private void configure(String filename) { logger.debug("LookupTable configure called"); if (!configured) { readTheFile(filename); configured = true; } } /** * returns the keys of map in its original order in the map * * @return array list of keys */ private ArrayList<Object> getKeys() { return keys; } /** * Tests this class * * @param args * @throws DeviceException */ public static void main(String[] args) throws DeviceException { System.out.println("testing LookupTable file reading ...."); LookupTable lut = new LookupTable(); // lut.configure("C:\\workspace\\config\\i11-EpicsSimulation\\lookupTables\\Automated_energy_setup.txt"); lut.configure("/scratch/i12workspace/i12-config/lookupTables/tomo/module_lookup_table.txt"); MultiValueMap lum = lut.getLookupMap(); for (Object key : lut.getKeys()) { Collection<?> list = lum.getCollection(key); for (Object o : list) { System.out.print(o + "\t"); } System.out.println(); } lut.getLookupKeys(); } @Override public void addIObserver(IObserver anIObserver) { observableComponent.addIObserver(anIObserver); } @Override public void deleteIObserver(IObserver anIObserver) { observableComponent.deleteIObserver(anIObserver); } @Override public void deleteIObservers() { observableComponent.deleteIObservers(); } /** * Notify all observers on the list of the requested change. * * @param theObserved * the observed component * @param theArgument * the data to be sent to the observer. */ public void notifyIObservers(Object theObserved, Object theArgument) { observableComponent.notifyIObservers(theObserved, theArgument); } @Override public boolean isLocal() { return this.local; } @Override public void setLocal(boolean local) { this.local = local; } }