/* * Copyright ThinkTank Maths Limited 2006 - 2008 * * This file 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 3 of the License, or (at your option) * any later version. * * This file 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 file. If not, see <http://www.gnu.org/licenses/>. */ package com.openlapi; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import javax.microedition.rms.RecordEnumeration; import javax.microedition.rms.RecordStore; import javax.microedition.rms.RecordStoreException; import javax.microedition.rms.RecordStoreFullException; import javax.microedition.rms.RecordStoreNotFoundException; import javax.microedition.rms.RecordStoreNotOpenException; /* * TODO: memoryless Enumeration of Landmark objects Currently, all Enumerations are * Vectors.elements(), but it would be much more scalable if the Enumeration accessed each * Landmark as the next element was requested, although this does make the store less * stable if there are more than one thread accessing the store. */ /** * The <code>LandmarkStore</code> class provides methods to store, delete and retrieve * landmarks from a persistent landmark store. There is one default landmark store and * there may be multiple other named landmark stores. The implementation may support * creating and deleting landmark stores by the application. All landmark stores MUST be * shared between all J2ME applications and MAY be shared with native applications in the * terminal. Named landmark stores have unique names in this API. If the underlying * implementation allows multiple landmark stores with the same name, it must present them * with unique names in the API e.g. by adding some postfix to those names that have * multiple instances in order to differentiate them. * <p> * Because native landmark stores may be stored as files in a file system and file systems * have sometimes limitations for the allowed characters in file names, the * implementations MUST support all other Unicode characters in landmark store names * except the following list: * <ul> * <li>0x0000...0x001F control characters</li> * <li>0x005C <code>'\'</code></li> * <li>0x002F <code>'/'</code></li> * <li>0x003A <code>':'</code></li> * <li>0x002A <code>'*'</code></li> * <li>0x003F <code>'?'</code></li> * <li>0x0022 <code>'"'</code></li> * <li>0x003C <code>'<'</code></li> * <li>0x003E <code>'>'</code></li> * <li>0x007C <code>'|'</code></li> * <li>0x007F...0x009F control characters</li> * <li>0xFEFF Byte-order-mark</li> * <li>0xFFF0...0xFFFF</li> * </ul> * Support for the listed characters is not required and therefore applications are * strongly encouraged not to use the characters listed above in landmark store names in * order to ensure interoperability of the application on different platform * implementations. * <p> * The <code>Landmark</code>s have a name and may be placed in a category or several * categories. The category is intended to group landmarks that are of similar type to the * end user, e.g. restaurants, museums, etc. The landmark names are strings that identify * the landmark to the end user. The category names describe the category to the end user. * The language used in the names may be any and depends on the preferences of the end * user. The names of the categories are unique within a <code>LandmarkStore</code>. * However, the names of the landmarks are not guaranteed to be unique. * <code>Landmark</code>s with the same name can appear in multiple categories or even * several <code>Landmark</code>s with the same name in the same category. * <p> * The <code>Landmark</code> objects returned from the <code>getLandmarks</code> * methods in this class shall guarantee that the application can read a consistent set of * the landmark data valid at the time of obtaining the object instance, even if the * landmark information in the store is modified subsequently by this or some other * application. * <p> * The <code>Landmark</code> object instances can be in two states: <br> * <ul> * <li>initially constructed by an application </li> * <li>belongs to a <code>LandmarkStore</code> </li> * </ul> * A <code>Landmark</code> object belongs to a <code>LandmarkStore</code> if it has * been obtained from the <code>LandmarkStore</code> using <code>getLandmarks</code> * or if it has been added to the <code>LandmarkStore</code> using * <code>addLandmark</code>. A <code>Landmark</code> object is initially constructed * by an application when it has been constructed using the constructor but has not been * added to a <code>LandmarkStore</code> using <code>addLandmark</code>. * <p> * Note that the term "belongs to a <code>LandmarkStore</code>" is defined above in a * way that "belong to a <code>LandmarkStore</code>" has an different meaning than the * landmark "being currently stored in a <code>LandmarkStore</code>". According to the * above definition, a <code>Landmark</code> object instance may be in a state where it * is considered to "belong to a <code>LandmarkStore</code>" even when it is not stored * in that <code>LandmarkStore</code> (e.g. if the landmark is deleted from the * <code>LandmarkStore</code> using <code>deleteLandmark</code> method, the * <code>Landmark</code> object instance still is considered to "belong to this * <code>LandmarkStore</code>"). * <p> * The landmark stores created by an application and landmarks added in landmark stores * persist even if the application itself is deleted from the terminal. * <p> * Accessing the landmark store may cause a <code>SecurityException</code>, if the * calling application does not have the required permissions. The permissions to read and * write (including add and delete) landmarks are distinct. An application having e.g. a * permission to read landmarks wouldn't necessarily have the permission to delete them. * The permissions (names etc.) for the MIDP 2.0 security framework are defined elsewhere * in this specification. */ public class LandmarkStore { /* * Overview of implementation (for JSR-179 hackers interested in the guts) * The Landmark store is implemented as a RecordStore (part of the MIDP * specification). The LandmarkStore name is prefixed by RECORD_STORE_PREFIX * to create the RecordStore name on disc. This is so that we don't clash * with other stores using the same name in completely unrelated stores for * other applications. We never save permanent temporary local versions of * the Landmarks as we don't want to have to store them all in memory. * WeakReferences are used to keep track of instances. This may have the * strange side effect that Landmarks may be returned that have been edited * by the same program but not saved to disc. e.g. An application gets all * Landmarks from the store and edits the Landmark for "Edinburgh Castle" * and changes it's name to "Scottish Castle". The same application then * emmediately requests all landmarks in Edinburgh. The returned Landmark * will include the edited one, not the one on disc. It will not be saved to * disc until .updateLandmark() is called on it. Permission-wise, the * default store is editable by all but new stores are only writable by the * app that created them in the first place. LandmarkStores may be read by * other apps. Note that the spec does not require additional stores to be * supported by the implementation and there is no standard policy regarding * the permission of such stores. This policy was chosen as it seems the * most sensible. Due to the lack of Serialization support in J2ME, this * contains private static methods to serialise and deserialise the Landmark * objects. Included in the serialisation is an unspecified number of * category Strings. We define a class named CategorisedLandmark which is * simply a container class for a Landmark and an array of String * categories, it is these that are actually serialised. Every LandmarkStore * has a record that holds the valid categories. Unfortunately the * RecordStore spec does not allow writing to an arbitrary record ID if it * doesn't exist (MPowerPlayer certainly does not allow this), nor is there * a standard on the ID of the first record (various implementations use 0 * or 1). It gets worse... the RecordEnumeration does not quarantee that its * first entry will be the first record, so each new instance of * LandmarkStore must first find out which record holds the category * information. The only way to do this is to attempt to read all the * records until one is only Strings, then record this ID in a local * variable. This ID will never change unless something evil accesses the * RecordStore directly or the implementation is completely moronic and * changes record IDs on the fly. The first (ID unspecified) record in the * store will always be a simple array of valid category Strings. By storing * this information in the RecordStore and not locally in the instance of * the LandmarkStore, it helps to keep the store syncronised across * different applications using the same store at the same time, although * due to the nature of the RecordStore, syncronisation cannot be guaranteed * and there will always be classic database race conditions. Future work * may involve making the get* methods memoryless. The spec requires that * they return Enumerations over Landmark objects. Currently we load all the * Landmark objects and return them as a live Enumeration, however a more * scalable solution would involve only reading the record from the disc * when requested. In terms of compliance with the spec... this does a good * job, but because of the lack of SecurityException throws in RecordStore * for read operations, most security-related denials of service will be * reported as IOExceptions. */ /** * For the purposes of serialisation of the AddressInfo object, this is * shared between {@link #serialiseLandmark(Landmark)} and * {@link #landmarkFromSerialised(byte[])} */ private static final int[] ADDRESS_INFO_ORDER = { AddressInfo.BUILDING_FLOOR, AddressInfo.BUILDING_NAME, AddressInfo.BUILDING_ROOM, AddressInfo.BUILDING_ZONE, AddressInfo.CITY, AddressInfo.COUNTRY, AddressInfo.COUNTRY_CODE, AddressInfo.COUNTY, AddressInfo.CROSSING1, AddressInfo.CROSSING2, AddressInfo.DISTRICT, AddressInfo.EXTENSION, AddressInfo.PHONE_NUMBER, AddressInfo.POSTAL_CODE, AddressInfo.STATE, AddressInfo.STREET, AddressInfo.URL }; /** * It is not possible to save a null string, so we use this as a * placeholder. It is important that null strings be recovered otherwise pre * and post stored objects will not have the same fields. Note that this * means that the literal String "(null)" will be recovered as a null, but * such a situation should be incredibly rare. */ private static final String NULL_STRING = "(null)"; /** * The LandmarkStore name is prefixed by this in order to get the name of * the RecordStore we save on the device. #see * {@link #recordStoreName(String)} */ private static final String RECORD_STORE_PREFIX = "jsr179_"; /** * The specification defines when to throw exceptions for filenames that are * too long, but not a minimal length. However, the RecordStore imposes a 32 * char maximum. */ private static final int STORE_NAME_MAX_CHARS = 32 - RECORD_STORE_PREFIX.length(); /** * Creates a new landmark store with a specified name. All LandmarkStores * are shared between all J2ME applications and may be shared with native * applications. Implementations may support creating landmark stores on a * removable media. However, the Java application is not able to directly * choose where the landmark store is stored, if the implementation supports * several storage media. The implementation of this method may e.g. prompt * the end user to make the choice if the implementation supports several * storage media. If the landmark store is stored on a removable media, this * media might be removed by the user possibly at any time causing it to * become unavailable. * <p> * A newly created landmark store does not contain any landmarks. * <p> * Note that the landmark store name MAY be modified by the implementation * when the store is created, e.g. by adding an implementation specific * post-fix to differentiate stores on different storage drives as described * in the class overview. Therefore, the application needs to use the * listLandmarkStores method to discover the form the name was stored as. * However, when creating stores to the default storage location, it is * recommended that the implementation does not modify the store name but * preserves it in the form it was passed to this method. It is strongly * recommended that this method is implemented as character case preserving * for the store name. * * @param storeName * the name of the landmark store to create * @throws NullPointerException * if the parameter is null * @throws IllegalArgumentException * if the name is too long or if a landmark store with the * specified name already exists * @throws IOException * if the landmark store couldn't be created due to an I/O error * @throws SecurityException * if the application does not have permissions to create a new * landmark store * @throws LandmarkException * if the implementation does not support creating new landmark * stores */ public static void createLandmarkStore(String storeName) throws NullPointerException, IllegalArgumentException, IOException, LandmarkException, SecurityException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.management"); // not allowed to create default stores if (storeName == null) throw new NullPointerException(); // outsource all the work createLandmarkStore2(storeName); } /** * Delete a landmark store with a specified name. All the landmarks and * categories defined in the named landmark store are irrevocably removed. * If a landmark store with the specified name does not exist, this method * returns silently without any error. * <p> * Note that the landmark store names MAY be handled as either * case-sensitive or case-insensitive (e.g. Unicode collation algorithm * level 2). Therefore, the implementation MUST accept the names in the form * returned by listLandmarkStores and MAY accept the name in other * variations of character case. * * @param storeName * the name of the landmark store to delete * @throws NullPointerException * if the parameter is null (the default landmark store can't be * deleted) * @throws IOException * if the landmark store couldn't be deleted due to an I/O error * @throws SecurityException * if the application does not have permissions to delete a * landmark store * @throws LandmarkException * if the implementation does not support deleting landmark * stores */ public static void deleteLandmarkStore(String storeName) throws NullPointerException, IOException, SecurityException, LandmarkException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.management"); if (storeName == null) throw new NullPointerException(); String recordStoreName = recordStoreName(storeName); try { // check if we have write permissions first RecordStore store = RecordStore.openRecordStore(recordStoreName, false); // this may throw SecurityException store.addRecord(null, 0, 0); // delete RecordStore.deleteRecordStore(recordStoreName); } catch (RecordStoreNotFoundException e) { return; } catch (RecordStoreException e) { throw new IOException(); } } /** * Gets a LandmarkStore instance for storing, deleting and retrieving * landmarks. There must be one default landmark store and there may be * other landmark stores that can be accessed by name. Note that the * landmark store names MAY be handled as either case-sensitive or * case-insensitive (e.g. Unicode collation algorithm level 2). Therefore, * the implementation MUST accept the names in the form returned by * listLandmarkStores and MAY accept the name in other variations of * character case. * * @param storeName * the name of the landmark store to open. if null, the default * landmark store will be returned * @return the LandmarkStore object representing the specified landmark * store or null if a landmark store with the specified name does * not exist. * @throws SecurityException * if the application does not have a permission to read * landmark stores */ public static LandmarkStore getInstance(String storeName) throws SecurityException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.read"); /* * Create a new LandmarkStore object and return it. If it has not been * created return null, if the permissions are not correct throw a * SecurityException, if there was an IO error return null. */ try { return new LandmarkStore(storeName); } catch (IOException e) { return null; } } /** * Lists the names of all the available landmark stores. The default * landmark store is obtained from getInstance by passing null as the * parameter. The null name for the default landmark store is not included * in the list returned by this method. If there are no named landmark * stores, other than the default landmark store, this method returns null. * <p> * The store names must be returned in a form that is directly usable as * input to getInstance and deleteLandmarkStore. * * @return an array of landmark store names * @throws SecurityException * if the application does not have the permission to access * landmark stores * @throws IOException * if an I/O error occurred when trying to access the landmark * stores */ public static String[] listLandmarkStores() throws SecurityException, IOException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.read"); // obtain a list of all RecordStores on the device. String[] allStores = RecordStore.listRecordStores(); Vector vecLandmarkStores = new Vector(); // if the string doesn't begin with #STORE_PREFIX, ignore it for (int i = 0; i < allStores.length; i++) { String storeName = allStores[i]; if (storeName.startsWith(RECORD_STORE_PREFIX)) { // trim the prefix when reporting back vecLandmarkStores.addElement(storeName.substring(RECORD_STORE_PREFIX.length())); } } if (vecLandmarkStores.size() == 0) return null; // need to return Array, not Vector String[] list = new String[vecLandmarkStores.size()]; Enumeration en = vecLandmarkStores.elements(); for (int i = 0; en.hasMoreElements(); i++) { list[i] = (String) en.nextElement(); } return list; } /** * A private version of {@link #createLandmarkStore(String)} that can create * the default store (null). * * @param storeName * @throws IllegalArgumentException * @throws IOException * @throws LandmarkException * @throws SecurityException */ private static void createLandmarkStore2(String storeName) throws IllegalArgumentException, IOException, LandmarkException, SecurityException { // check if it exists already, throw appropriate Exception if it does String recordStoreName = recordStoreName(storeName); String[] existAlready = RecordStore.listRecordStores(); if (existAlready != null) { for (int i = 0; i < existAlready.length; i++) { if (existAlready[i].equals(recordStoreName)) throw new IllegalArgumentException(); } } // try this entire block, there are many places throwing the same // exceptions try { RecordStore store; if (storeName == null) { // we were asked to create the default store store = RecordStore.openRecordStore(recordStoreName(storeName), true); // setMode() may throw SecurityException so we don't have to // handle it. store.setMode(RecordStore.AUTHMODE_ANY, true); } else { // not the default store // enforce a name length restriction if (storeName.length() > STORE_NAME_MAX_CHARS) throw new IllegalArgumentException("LocationStore name too long."); // check that the String is FS-safe for (int i = 0; i < storeName.length(); i++) { if (isUnsupportedUnicode(storeName.charAt(i))) throw new IllegalArgumentException("LocationStore name does not support some unicode characters."); } // create the underlying RecordStore. Readable, but not writable // by any other MIDlets. setMode() may throw SecurityException // so we don't have to handle it. store = RecordStore.openRecordStore(recordStoreName(storeName), true); store.setMode(RecordStore.AUTHMODE_ANY, false); } // place a null marker string in the category record ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); out.writeUTF(NULL_STRING); byte[] b = baos.toByteArray(); store.addRecord(b, 0, b.length); } catch (RecordStoreFullException e) { throw new LandmarkException(e.getMessage()); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } /* * There is no need to create an actual LandmarkStore object, as calling * getInstance() will return one from the RecordStore we just created on * the device. */ } /** * Helper method that takes raw byte data for a single Landmark object as * input and returns a CategorisedLandmark object. This is sadly needed as * neither Landmark nor its components implement Serializable in the * specification. Not that it matters since ObjectStream doesn't exist in * J2ME. * * @see #serialise(CategorisedLandmark) * @return * @throws IOException */ private static CategorisedLandmark deserialise(byte[] rawBytes) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream(rawBytes); DataInputStream in = new DataInputStream(bais); // read the data from the byte stream, in the same order as it was saved // replace empty strings with null strings String name = in.readUTF(); if (name.equals(NULL_STRING)) // this should *never* happen unless the file was edited directly or // the Landmark really had an empty (but not null) name. throw new IOException(); String description = in.readUTF(); if (description.equals(NULL_STRING)) { description = null; } String[] addressInfoParts = new String[ADDRESS_INFO_ORDER.length]; for (int i = 0; i < ADDRESS_INFO_ORDER.length; i++) { addressInfoParts[i] = in.readUTF(); } float altitude = in.readFloat(); double latitude = in.readDouble(); double longitude = in.readDouble(); float horizontalAccuracy = in.readFloat(); float verticalAccuracy = in.readFloat(); // read the categories Vector vecCategories = new Vector(); while (true) { String category; try { category = in.readUTF(); } catch (EOFException e) { break; } // ignore null Strings, which indicate that there are no categories if (!category.equals(NULL_STRING)) { vecCategories.addElement(category); } } // construct a new QualifiedCoordinates object QualifiedCoordinates qualifiedCoordinates = new QualifiedCoordinates(latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy); // construct a new AddressInfo object, ignoring empty strings AddressInfo addressInfo = new AddressInfo(); for (int i = 0; i < ADDRESS_INFO_ORDER.length; i++) { String part = addressInfoParts[i]; if (!part.equals(NULL_STRING)) { addressInfo.setField(ADDRESS_INFO_ORDER[i], part); } } // construct a new Landmark object Landmark landmark; landmark = new Landmark(name, description, qualifiedCoordinates, addressInfo); // construct the CategorisedLandmark object CategorisedLandmark catLandmark = new CategorisedLandmark(landmark); // add the categories Enumeration en = vecCategories.elements(); for (; en.hasMoreElements();) { String category = (String) en.nextElement(); catLandmark.addCategory(category); } return catLandmark; } /** * Returns true if the char is one of * <ul> * <li>0x0000...0x001F control characters</li> * <li>0x005C <code>'\'</code></li> * <li>0x002F <code>'/'</code></li> * <li>0x003A <code>':'</code></li> * <li>0x002A <code>'*'</code></li> * <li>0x003F <code>'?'</code></li> * <li>0x0022 <code>'"'</code></li> * <li>0x003C <code>'<'</code></li> * <li>0x003E <code>'>'</code></li> * <li>0x007C <code>'|'</code></li> * <li>0x007F...0x009F control characters</li> * <li>0xFEFF Byte-order-mark</li> * <li>0xFFF0...0xFFFF</li> * </ul> * * @param character * @return true if the character is one of the unicode characters defined in the * specification where support is not required. */ private static boolean isUnsupportedUnicode(char character) { // the individually labelled unicode values of unsupported characters int[] individuals = { 0x005C, 0x002F, 0x003A, 0x002A, 0x003F, 0x0022, 0x003C, 0x003E, 0x007C, 0xFEFF }; for (int i = 0; i < individuals.length; i++) { if (character == individuals[i]) return true; } // the block-defined unicode values of unsupported characters // be careful when using this. the even indexed entries are where a // block begins, ending with the proceeding odd indexed entry. int[] blocks = { 0x0000, 0x001F, 0x007F, 0x009F, 0xFFF0, 0xFFFF }; for (int i = 0; i < blocks.length; i = i + 2) { if ((character >= blocks[i]) && (character <= blocks[i + 1])) return true; } return false; } /** * Helper method that calculates the name of the RecordStore associated to a * LandmarkStore name. * * @param storeName * @return */ private static String recordStoreName(String storeName) { // this is the identifier we use for the default store. Using an empty // string is probably the safest option. if (storeName == null) { storeName = ""; } return RECORD_STORE_PREFIX + storeName; } /** * Helper method that takes a CategorisedLandmark object as input and * returns a raw byte array which can be saved in the store. This is sadly * needed as neither Landmark nor its components implement Serializable in * the specification. Not that it matters since ObjectStream doesn't exist * in J2ME. * * @see #unserialised(byte[]) * @return * @throws IOException */ private static byte[] serialise(CategorisedLandmark catLandmark) throws IOException { Landmark landmark = catLandmark.getLandmark(); String[] categories = catLandmark.getCategories(); // get the top-level components String name = landmark.getName(); String description = landmark.getDescription(); AddressInfo addressInfo = landmark.getAddressInfo(); QualifiedCoordinates qualifiedCoordinates = landmark.getQualifiedCoordinates(); // get the AddressInfo components String[] addressInfoParts = new String[ADDRESS_INFO_ORDER.length]; for (int i = 0; i < ADDRESS_INFO_ORDER.length; i++) { addressInfoParts[i] = addressInfo.getField(ADDRESS_INFO_ORDER[i]); } // get the QualifiedCoordinates components float altitude = qualifiedCoordinates.getAltitude(); double latitude = qualifiedCoordinates.getLatitude(); double longitude = qualifiedCoordinates.getLongitude(); float horizontalAccuracy = qualifiedCoordinates.getHorizontalAccuracy(); float verticalAccuracy = qualifiedCoordinates.getVerticalAccuracy(); // now convert this data into a bytestream ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); // note that writeUTF(null) may screw things up, so save a marker out.writeUTF(name == null ? NULL_STRING : name); out.writeUTF(description == null ? NULL_STRING : description); for (int i = 0; i < addressInfoParts.length; i++) { String part = addressInfoParts[i]; out.writeUTF(part == null ? NULL_STRING : part); } out.writeFloat(altitude); out.writeDouble(latitude); out.writeDouble(longitude); out.writeFloat(horizontalAccuracy); out.writeFloat(verticalAccuracy); // place the categories at the end, as there are an unspecified amount if (categories == null) { // if there were none, store the null marker out.writeUTF(NULL_STRING); } else { for (int i = 0; i < categories.length; i++) { String category = categories[i]; out.writeUTF(category == null ? NULL_STRING : category); } } // extract the byte array byte[] b = baos.toByteArray(); return b; } /** * The instance specific record ID of the category info. */ private int categoryID; /** * In order to allow .updateLandmarks() to work correctly, we need to track * which instances are associated to the entries in the store. We could * simply keep a cache of every elemnt that has been requested but that * would involve storing all elements in memory! Not really an option for * even 100k stores on a mobile device. Instead we keep a hashTable of IDs * to weakReferences. * <p> * Note that although a weakReference may exist here, do not forget that * another instance of the LandmarkStore may have changed the data on disc, * so existence here does not mean existence on disc. * <p> * Map is from Record ID (Integer) to Landmark (WeakReference) */ private final Hashtable instances = new Hashtable(); /** * This is where the data is persistently stored. */ private RecordStore store; /** * Private constructor so that stores may only be created using the static * methods of this class. * * @throws IOException * @throws SecurityException */ private LandmarkStore(String storeName) throws IOException, SecurityException { String recordStoreName = recordStoreName(storeName); try { // only open, do not create if it doesn't exist store = RecordStore.openRecordStore(recordStoreName, false); } catch (RecordStoreException e) { // the Exception might be because we asked for the default store and // it was never created, in which case create it. if (storeName == null) { try { createLandmarkStore2(null); // don't forget to open the default store after we create it store = RecordStore.openRecordStore(recordStoreName, false); } catch (IllegalArgumentException e2) { throw new IOException(e2.getMessage()); } catch (LandmarkException e2) { throw new IOException(e2.getMessage()); } catch (RecordStoreException e2) { throw new IOException(e2.getMessage()); } } else throw new IOException(e.getMessage()); } // determine where the category info is stored determineCategoryID(); } /** * Adds a category to this LandmarkStore. All implementations must support * names that have length up to and including 32 characters. If the provided * name is longer it may be truncated by the implementation if necessary. * * @param categoryName * name for the category to be added * @throws IllegalArgumentException * if a category with the specified name already exists * @throws NullPointerException * if the parameter is null * @throws LandmarkException * if this LandmarkStore does not support adding new categories * @throws IOException * if an I/O error occurs or there are no resources to add a new * category * @throws SecurityException * if the application does not have the permission to manage * categories */ public void addCategory(String categoryName) throws IllegalArgumentException, NullPointerException, LandmarkException, IOException, SecurityException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.category"); if (categoryName == null) throw new NullPointerException(); // truncate if necessary if (categoryName.length() > 32) { categoryName = categoryName.substring(0, 32); } // get the latest category list Vector categories = getCategories1(); // does it exist already if (categories.contains(categoryName)) throw new IllegalArgumentException(); // add to the category list categories.addElement(categoryName); // save to disc saveCategories(categories); } /** * Adds a landmark to the specified group in the landmark store. If some * textual String field inside the landmark object is set to a value that is * too long to be stored, the implementation is allowed to automatically * truncate fields that are too long. * <p> * However, the name field MUST NOT be truncated. Every implementation shall * be able to support name fields that are 32 characters or shorter. * Implementations may support longer names but are not required to. If an * application tries to add a Landmark with a longer name field than the * implementation can support, IllegalArgumentException is thrown. * <p> * When the landmark store is empty, every implementation is required to be * able to store a landmark where each String field is set to a 30 character * long string. * <p> * If the Landmark object that is passed as a parameter is an instance that * belongs to this LandmarkStore, the same landmark instance will be added * to the specified category in addition to the category/categories which it * already belongs to. If the landmark already belongs to the specified * category, this method returns with no effect. If the landmark has been * deleted after obtaining it from getLandmarks, it will be added back when * this method is called. * <p> * If the Landmark object that is passed as a parameter is an instance * initially constructed by the application using the constructor or an * instance that belongs to a different LandmarkStore, a new landmark will * be created in this LandmarkStore and it will belong initially to only the * category specified in the category parameter. After this method call, the * Landmark object that is passed as a parameter belongs to this * LandmarkStore. * * @param landmark * the landmark to be added * @param category * where the landmark is added. null can be used to indicate that * the landmark does not belong to a category * @throws SecurityException * if the application is not allowed to add landmarks * @throws IllegalArgumentException * if the landmark has a longer name field than the * implementation can support or if the category is not null or * one of the categories defined in this LandmarkStore * @throws IOException * if an I/O error happened when accessing the landmark store or * if there are no resources available to store this landmark * @throws NullPointerException * if the landmark parameter is null */ public void addLandmark(Landmark landmark, String category) throws SecurityException, IllegalArgumentException, IOException, NullPointerException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.write"); if (landmark == null) throw new NullPointerException(); // check if the category is valid Vector validCategories = getCategories1(); if ((category != null) && !validCategories.contains(category)) throw new IllegalArgumentException(); // check if an instance already exists int ID = getRecordIDOfInstance(landmark); if (ID != -1) { // already in the store, add category CategorisedLandmark clm = getCLAtID(ID); clm.addCategory(category); saveCLToID(clm, ID); return; } // no instance found in the store // create the CategorisedLandmark object CategorisedLandmark clm = new CategorisedLandmark(landmark); if (category != null) { clm.addCategory(category); } // save to the store saveNewCL(clm); } /** * Removes a category from this LandmarkStore. The category will be removed * from all landmarks that are in that category. However, this method will * not remove any of the landmarks, only the associated category information * from the landmarks. If a category with the supplied name does not exist * in this LandmarkStore, the method returns silently with no error. * * @param categoryName * name for the category to be removed * @throws NullPointerException * if the parameter is null * @throws LandmarkException * if this LandmarkStore does not support deleting categories * @throws IOException * if an I/O error occurs * @throws SecurityException * if the application does not have the permission to manage * categories */ public void deleteCategory(String categoryName) throws NullPointerException, LandmarkException, IOException, SecurityException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.category"); if (categoryName == null) throw new NullPointerException(); // remove the category from the valid list Vector categories = getCategories1(); if (!categories.contains(categoryName)) // it wasn't a valid category anyway, return silently return; categories.removeElement(categoryName); saveCategories(categories); // remove Landmarks from this category int[] records = getRecordIDs(); if (records == null) return; for (int i = 0; i < records.length; i++) { // get the ID int ID = records[i]; // get the record byte[] record; try { record = store.getRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } CategorisedLandmark clm = deserialise(record); // check if the Landmark is in the category if (clm.inCategory(categoryName)) { // it is! // remove the category from the landmark clm.removeCategory(categoryName); // save the new Categorised Landmark saveCLToID(clm, ID); } } } /** * Deletes a landmark from this LandmarkStore. This method removes the * specified landmark from all categories and deletes the information from * this LandmarkStore. The Landmark instance passed in as the parameter must * be an instance that belongs to this LandmarkStore. * <p> * If the Landmark belongs to this LandmarkStore but has already been * deleted from this LandmarkStore, then the request is silently ignored and * the method call returns with no error. Note that LandmarkException is * thrown if the Landmark instance does not belong to this LandmarkStore, * and this is different from not being stored currently in this * LandmarkStore. * * @param lm * the landmark to be deleted * @throws SecurityException * if the application is not allowed to delete the landmark * @throws LandmarkException * if the landmark instance passed as the parameter does not * belong to this LandmarkStore * @throws IOException * if an I/O error happened when accessing the landmark store * @throws NullPointerException * if the parameter is null */ public void deleteLandmark(Landmark lm) throws SecurityException, LandmarkException, IOException, NullPointerException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.write"); if (lm == null) throw new NullPointerException(); // check if an instance actually exists int ID = getRecordIDOfInstance(lm); if (ID == -1) // instance not from this store throw new LandmarkException(); // delete from disc try { store.deleteRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } // remove from our lookup table instances.remove(new Integer(ID)); } /** * Returns the category names that are defined in this LandmarkStore. The * language and locale used for these names depends on the implementation * and end user settings. The names shall be such that they can be displayed * to the end user and have a meaning to the end user. * * @return an Enumeration containing Strings representing the category * names. If there are no categories defined in this LandmarkStore, * an Enumeration with no entries is returned. */ public Enumeration getCategories() { Vector categories; try { categories = getCategories1(); } catch (IOException e) { // hmm... no way to report back if any error occured, so just return // null if anything bad happened return null; } // if there were no categories, return an enumeration with no entries if (categories == null) { Vector empty = new Vector(0); return empty.elements(); } return categories.elements(); } /** * Lists all landmarks stored in the store. * * @return an Enumeration object containing Landmark objects representing * all the landmarks stored in this LandmarkStore or null if there * are no landmarks in the store * @throws IOException * if an I/O error happened when accessing the landmark store */ public Enumeration getLandmarks() throws IOException { // we will create a Vector of Landmark objects Vector landmarks = new Vector(); // hold a temporary list of instance lookups, incase something throws an // Exception before we return Hashtable lookup = new Hashtable(); // cycle through all the records int[] records = getRecordIDs(); if (records == null) return null; for (int i = 0; i < records.length; i++) { // get the ID int ID = records[i]; // get the record byte[] record; try { record = store.getRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } // is an instance already alive Landmark lm = getAliveLandmark(ID); if (lm != null) { // yes, an instance is already alive landmarks.addElement(lm); continue; } // no instance currently alive CategorisedLandmark clm = deserialise(record); // add the landmark, ignore the category info lm = clm.getLandmark(); landmarks.addElement(lm); // record the ID and instance WeakReference instance = new WeakReference(lm); lookup.put(new Integer(ID), instance); } // don't forget to update the instance ID lookup instanceTableUpdate(lookup); return landmarks.elements(); } /** * Lists all the landmarks that are within an area defined by bounding * minimum and maximum latitude and longitude and belong to the defined * category, if specified. The bounds are considered to belong to the area. * If minLongitude <= maxLongitude, this area covers the longitude range * [minLongitude, maxLongitude]. If minLongitude > maxLongitude, this area * covers the longitude range [-180.0, maxLongitude] and [minLongitude, * 180.0). * <p> * For latitude, the area covers the latitude range [minLatitude, * maxLatitude]. * * @param category * the category of the landmark. null implies a wildcard that * matches all categories * @param minLatitude * minimum latitude of the area. Must be within the range [-90.0, * 90.0] * @param maxLatitude * maximum latitude of the area. Must be within the range * [minLatitude, 90.0] * @param minLongitude * minimum longitude of the area. Must be within the range * [-180.0, 180.0) * @param maxLongitude * maximum longitude of the area. Must be within the range * [-180.0, 180.0) * @return an Enumeration containing all the matching Landmarks or null if * no Landmark matched the given parameters * @throws IOException * if an I/O error happened when accessing the landmark store * @throws IllegalArgumentException * if the minLongitude or maxLongitude is out of the range * [-180.0, 180.0), or minLatitude or maxLatitude is out of the * range [-90.0,90.0], or if minLatitude > maxLatitude */ public Enumeration getLandmarks(String category, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) throws IOException, IllegalArgumentException { if ((minLatitude > maxLatitude) || (minLatitude < -90) || (maxLatitude > 90) || (minLongitude < -180) || (minLongitude >= 180) || (maxLongitude < -180) || (maxLongitude >= 180)) throw new IllegalArgumentException(); // we will create a Vector of Landmark objects Vector landmarks = new Vector(); // hold a temporary list of instance lookups, incase something throws an // Exception before we return Hashtable lookup = new Hashtable(); // cycle through all the records int[] records = getRecordIDs(); if (records == null) return null; for (int i = 0; i < records.length; i++) { // get the ID int ID = records[i]; // get the record byte[] record; try { record = store.getRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } // no point looking to see if it's alive yet, as we need the // category info CategorisedLandmark clm = deserialise(record); // is the Landmark categorised correctly if ((category == null) || clm.inCategory(category)) { // is the Landmark in the correct region Landmark lm = clm.getLandmark(); QualifiedCoordinates qc = lm.getQualifiedCoordinates(); double longitude = qc.getLongitude(); double latitude = qc.getLatitude(); // first test latitude if ((latitude < minLatitude) || (latitude > maxLatitude)) { continue; } // then the longitude, urgh if (((minLongitude <= maxLongitude) && (longitude >= minLongitude) && (longitude <= maxLongitude)) || ((minLongitude > maxLongitude) && (((longitude >= -180) && (longitude < maxLongitude)) || ((longitude < 180) && (longitude > minLongitude))))) { // ok, this Landmark passes the tests // is an instance already alive Landmark aliveLm = getAliveLandmark(ID); if (aliveLm == null) { // no, not alive landmarks.addElement(lm); // record the instance WeakReference instance = new WeakReference(lm); lookup.put(new Integer(ID), instance); } else { // yes, an instance exists already landmarks.addElement(aliveLm); } } } } // don't forget to update the instance ID lookup instanceTableUpdate(lookup); return landmarks.elements(); } /** * Gets the Landmarks from the storage where the category and/or name * matches the given parameters. * * @param category * the category of the landmark. null implies a wildcard that * matches all categories * @param name * the name of the desired landmark. null implies a wildcard that * matches all the names within the category indicated by the * category parameter * @return an Enumeration containing all the matching Landmarks or null if * no Landmark matched the given parameters * @throws IOException * if an I/O error happened when accessing the landmark store */ public Enumeration getLandmarks(String category, String name) throws IOException { // we will create a Vector of Landmark objects Vector landmarks = new Vector(); // hold a temporary list of instance lookups, incase something throws an // Exception before we return Hashtable lookup = new Hashtable(); // cycle through all the records int[] records = getRecordIDs(); if (records == null) return null; for (int i = 0; i < records.length; i++) { // get the ID int ID = records[i]; // get the record byte[] record; try { record = store.getRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } // no point looking to see if it's alive yet, as we need the // category info CategorisedLandmark clm = deserialise(record); Landmark lm = clm.getLandmark(); // is the Landmark name OK, and categorised correctly if (((name == null) || lm.getName().equals(name)) && ((category == null) || clm.inCategory(category))) { // ok, this Landmark passes the tests // is an instance already alive Landmark aliveLm = getAliveLandmark(ID); if (aliveLm == null) { // no, not alive landmarks.addElement(lm); // record the instance WeakReference instance = new WeakReference(lm); lookup.put(new Integer(ID), instance); } else { // yes, an instance exists already landmarks.addElement(aliveLm); } } } // don't forget to update the instance ID lookup instanceTableUpdate(lookup); return landmarks.elements(); } /** * Removes the named landmark from the specified category. The Landmark * instance passed in as the parameter must be an instance that belongs to * this LandmarkStore. * <p> * If the Landmark is not found in this LandmarkStore in the specified * category or if the parameter is a Landmark instance that does not belong * to this LandmarkStore, then the request is silently ignored and the * method call returns with no error. The request is also silently ignored * if the specified category does not exist in this LandmarkStore. * <p> * The landmark is only removed from the specified category but the landmark * information is retained in the store. If the landmark no longer belongs * to any category, it can still be obtained from the store by passing null * as the category to getLandmarks. * * @param lm * the landmark to be removed * @param category * the category from which it will be removed. * @throws SecurityException * if the application is not allowed to delete the landmark * @throws IOException * if an I/O error happened when accessing the landmark store * @throws NullPointerException * if either parameter is null */ public void removeLandmarkFromCategory(Landmark lm, String category) throws SecurityException, IOException, NullPointerException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.write"); if ((lm == null) || (category == null)) throw new NullPointerException(); Vector categories = getCategories1(); if (!categories.contains(category)) // it wasn't a valid category anyway, return silently return; // get the category info for the Landmark int id = getRecordIDOfInstance(lm); CategorisedLandmark clm = getCLAtID(id); // remove the landmark from the category clm.removeCategory(category); // save the new clasification info to disc saveCLToID(clm, id); } /** * Updates the information about a landmark. This method only updates the * information about a landmark and does not modify the categories the * landmark belongs to. The Landmark instance passed in as the parameter * must be an instance that belongs to this LandmarkStore. * <p> * This method can't be used to add a new landmark to the store. * * @param lm * the landmark to be updated * @throws SecurityException * if the application is not allowed to update the landmark * @throws LandmarkException * if the landmark instance passed as the parameter does not * belong to this LandmarkStore or does not exist in the store * any more * @throws IOException * if an I/O error happened when accessing the landmark store * @throws NullPointerException * if the parameter is null * @throws IllegalArgumentException * if the landmark has a longer name field than the * implementation can support */ public void updateLandmark(Landmark lm) throws SecurityException, LandmarkException, IOException, NullPointerException, IllegalArgumentException { // test Security permissions OpenLAPICommon.testPermission("javax.microedition.location.LandmarkStore.write"); if (lm == null) throw new IllegalArgumentException(); // first check if lm is an instance recently obtained from the store int ID = getRecordIDOfInstance(lm); if (ID == -1) throw new LandmarkException(); // extract the category info CategorisedLandmark clm = getCLAtID(ID); CategorisedLandmark newClm = new CategorisedLandmark(lm); String[] categories = clm.getCategories(); if (categories != null) { for (int i = 0; i < categories.length; i++) { newClm.addCategory(categories[i]); } } // save the updated Landmark to disc saveCLToID(newClm, ID); } /** * Sets the local variable {@link #categoryID}. To be run when the * LandmarkStore is first instantiated (but valid anytime, though it should * never be needed after that, unless something other than this class edited * the RecordStore directly). * * @throws IOException */ private void determineCategoryID() throws IOException { try { RecordEnumeration en = store.enumerateRecords(null, null, false); for (; en.hasNextElement();) { int ID = en.nextRecordId(); byte[] record = store.getRecord(ID); if (isCategoryRecord(record)) { categoryID = ID; return; } } throw new IOException("No category info in RecordStore " + store.getName()); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } } /** * Convenience method for obtaining the instance associated to a record ID. * Will return the instance if it exists, otherwise null. * * @param id * @return */ private Landmark getAliveLandmark(int id) { Integer ID = new Integer(id); if (instances.contains(ID)) { WeakReference weakRef = (WeakReference) instances.get(ID); return (Landmark) weakRef.get(); } return null; } /** * Helper method that returns all the valid categories in this store. Reads * from the category record in the store from disc on each call to avoid * syncronisation issues. * * @return a String Vector of all categories. returns empty Vector if there * were none. * @throws RecordStoreException * @throws RecordStoreNotOpenException * @throws IOException */ private Vector getCategories1() throws IOException { byte[] raw; try { raw = store.getRecord(categoryID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } ByteArrayInputStream bais = new ByteArrayInputStream(raw); DataInputStream in = new DataInputStream(bais); // read the categories Vector vecCategories = new Vector(); while (true) { String category; try { category = in.readUTF(); } catch (EOFException e) { break; } // ignore null Strings, which indicate that there are no categories if (!category.equals(NULL_STRING)) { vecCategories.addElement(category); } } // return the categories return vecCategories; } /** * Convenience method for obtaining the CategorisedLandmark from a * particular record in the store. Note that it is very inefficient to use * this within an enumeration of the records, so only use it when you know * the ID and are not enumerating. Yes, it would be wonderful if RecordStore * returned a Collection of record IDs, but it doesn't. * * @see #saveCLToID(CategorisedLandmark, int) * @param ID * @return * @throws IOException */ private CategorisedLandmark getCLAtID(int ID) throws IOException { byte[] record; try { record = store.getRecord(ID); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } CategorisedLandmark clm = deserialise(record); return clm; } /** * @param lm * @return the record ID of an instance of a Landmark object, -1 if the * instance is not recently from the store. (Note this is not to say * that an exact duplicate of the Landmark isn't in the store). */ private int getRecordIDOfInstance(Landmark lm) { for (Enumeration e = instances.keys(); e.hasMoreElements();) { Integer ID = (Integer) e.nextElement(); WeakReference instance = (WeakReference) instances.get(ID); if (lm.equals(instance.get())) return ID.intValue(); } return -1; } /** * Retrieve an array containing all of the valid record IDs of the store, at * the moment it was called. If an ID is changed or removed by another * appliciton in between calling this method and accessing ing contents, * expect IOException. * <p> * This method only returns IDs that contain suspected CategorisedLandmark * objects (i.e. does not return category entries). * <p> * Returns null if there were no entries. * * @return * @throws IOException */ private int[] getRecordIDs() throws IOException { try { // number of records is store size minus the category entry int size = store.getNumRecords() - 1; if (size == 0) return null; int[] recordIDs = new int[size]; int i = 0; RecordEnumeration en = store.enumerateRecords(null, null, false); for (; en.hasNextElement();) { int recordID = en.nextRecordId(); if (recordID == categoryID) { continue; } // add it to the list recordIDs[i] = recordID; i++; } return recordIDs; } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } } /** * Helper method to merge a Hashtable of ID->hashCodes to the store's lookup * table. * * @param table */ private void instanceTableUpdate(Hashtable table) { for (Enumeration e = table.keys(); e.hasMoreElements();) { Integer ID = (Integer) e.nextElement(); WeakReference instance = (WeakReference) table.get(ID); // overwrite previous ID entries... assume that the caller knows // what they are doing and that the instance reference is now // garbage collected instances.put(ID, instance); } } /** * When given a byte array, determine if it is or is not the category entry. * * @param rawBytes * @return */ private boolean isCategoryRecord(byte[] rawBytes) { // rather than writing new code that looks for all Strings, check if it // is a CategorisedLandmark and if an Exception is thrown assume it is // the category info try { deserialise(rawBytes); } catch (IOException e) { // wasn't a CategorisedLandmark, must be categories return true; } // was a CategorisedLandmark return false; } /** * Convenience method to save a Vector of String objects to the category * entry of the record store. * * @param categories * @throws IOException */ private void saveCategories(Vector categories) throws IOException { // create the record ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); if (categories == null) { // if there were none, store the null marker out.writeUTF(NULL_STRING); } else { for (int i = 0; i < categories.size(); i++) { String category = (String) categories.elementAt(i); out.writeUTF(category == null ? NULL_STRING : category); } } // and save to disc byte[] b = baos.toByteArray(); try { store.setRecord(categoryID, b, 0, b.length); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } } /** * Convenience method for saving a CategorisedLandmark to a particular * record in the store. * * @see #getCLAtID(int) * @param clm * @param ID * @throws IOException */ private void saveCLToID(CategorisedLandmark clm, int ID) throws IOException { // serialise the new Categorised Landmark byte[] record = serialise(clm); // save to the RecordStore try { store.setRecord(ID, record, 0, record.length); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } } /** * Convenience method for appending a new CategorisedLandmark object to the * store. * * @param clm * @throws IOException */ private void saveNewCL(CategorisedLandmark clm) throws IOException { // serialise the new record byte[] record = serialise(clm); Integer ID; try { // save to disc and get the ID ID = new Integer(store.addRecord(record, 0, record.length)); } catch (RecordStoreException e) { throw new IOException(e.getMessage()); } // get the instance WeakReference instance = new WeakReference(clm.getLandmark()); // add to the instance lookup instances.put(ID, instance); } }