package com.rayo.server.storage; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import org.springframework.core.io.Resource; import com.rayo.server.storage.memory.InMemoryDatastore; import com.rayo.server.storage.model.Application; import com.rayo.server.storage.model.GatewayCall; import com.rayo.server.storage.model.GatewayClient; import com.rayo.server.storage.model.GatewayMixer; import com.rayo.server.storage.model.GatewayVerb; import com.rayo.server.storage.model.RayoNode; import com.voxeo.logging.Loggerf; /** * <p>A properties based datastore implementation intended for being used in Prism standalone. * This implementation is not intended for production usage but for being used by developers for * their own personal developments or prototypes as it does not require to maintain a separate * database or NoSQL system like other implementations do (e.g. {@link CassandraDatastore} ).</p> * * <p>As suggested above, this implementation does not support any clustering capabilities. It * is only intended for a single-box scenario.</p> * * <p>The implementation is backed up by a properties file with a very simple format that has * been traditionally use in Rayo standalone. The properties file is made of a simple list of * addresses and their applications. For example:</p> * * <pre> * sip:usera@localhost=app1@apps.tropo.com * sip:usera@localhost=app2@apps.tropo.com * </pre> * * <p>Wildcard characters and regular expresions can be used to map multiple addresses to a single * application. Like for example:</p> * * <pre> * *+13457800.*=usera@apps.tropo.com * .*sipusername.*=userb@apps.tropo.com * .*=userc@apps.tropo.com * </pre> * * <p>This implementation will reload periodically the contents of the properties file to * memory. This way users can add new addresses and application mappings to the datastore * either by using an exernal system like a provisioning database or by editing manually the * properties file that backs up this data store.</p> * * @author martin * */ public class PropertiesBasedDatastore implements GatewayDatastore { private static final Loggerf logger = Loggerf.getLogger(PropertiesBasedDatastore.class); Map<String, PropertiesValue> map = Collections.synchronizedMap(new LinkedHashMap<String, PropertiesValue>()); private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /* * This in memory datastore will be used to store all the data that cannot be stored * in the properties file. As the file is way too simple. */ private InMemoryDatastore delegateStore = new InMemoryDatastore(); private int failures; private Resource properties; public PropertiesBasedDatastore(final Resource properties) throws IOException { this(properties, 60000); } /** * Creates the properties datastore backed up with the given properties * file. * * @param properties Properties file with all the mappings * @param delay The amount of time that this datastore will wait until reloading the properties file * looking for changes. By default it is 60 seconds. * * @throws IOException If the service cannot be created */ public PropertiesBasedDatastore(final Resource properties, int delay) throws IOException { this.properties = properties; read(properties); TimerTask readTask = new TimerTask() { @Override public void run() { try { read(properties); } catch (IOException e) { failures++; } } }; new Timer().schedule(readTask, delay, delay); } @Override public void addCallToMixer(String callId, String mixerName) throws DatastoreException { delegateStore.addCallToMixer(callId, mixerName); } @Override public void addVerbToMixer(GatewayVerb verb, String mixerName) throws DatastoreException { delegateStore.addVerbToMixer(verb, mixerName); } @Override public void createFilter(String jid, String id) throws DatastoreException { delegateStore.createFilter(jid, id); } @Override public List<String> getAddressesForApplication(String jid) { return delegateStore.getAddressesForApplication(jid); } @Override public Application getApplication(String jid) { return delegateStore.getApplication(jid); } @Override public Application getApplicationForAddress(String address) { return delegateStore.getApplicationForAddress(address); } @Override public List<Application> getApplications() { return delegateStore.getApplications(); } @Override public GatewayCall getCall(String callId) { return delegateStore.getCall(callId); } @Override public Collection<String> getCalls() { return delegateStore.getCalls(); } @Override public Collection<String> getCallsForClient(String jid) { return delegateStore.getCallsForClient(jid); } @Override public Collection<String> getCallsForNode(String rayoNode) { return delegateStore.getCallsForNode(rayoNode); } @Override public GatewayClient getClient(String clientJid) { return delegateStore.getClient(clientJid); } @Override public List<String> getClientResources(String clientJid) { return delegateStore.getClientResources(clientJid); } @Override public List<String> getClients() { return delegateStore.getClients(); } @Override public List<String> getFilteredApplications(String id) throws DatastoreException { return delegateStore.getFilteredApplications(id); } @Override public GatewayMixer getMixer(String mixerName) { return delegateStore.getMixer(mixerName); } @Override public Collection<GatewayMixer> getMixers() { return delegateStore.getMixers(); } @Override public RayoNode getNode(String rayoNode) { return delegateStore.getNode(rayoNode); } @Override public String getNodeForCall(String callId) { return delegateStore.getNodeForCall(callId); } @Override public String getNodeForIpAddress(String ipAddress) { return delegateStore.getNodeForIpAddress(ipAddress); } @Override public Collection<String> getPlatforms() { return delegateStore.getPlatforms(); } @Override public List<RayoNode> getRayoNodesForPlatform(String platformId) { return delegateStore.getRayoNodesForPlatform(platformId); } @Override public GatewayVerb getVerb(String mixerName, String verbId) { return delegateStore.getVerb(mixerName, verbId); } @Override public List<GatewayVerb> getVerbs() { return delegateStore.getVerbs(); } public int hashCode() { return delegateStore.hashCode(); } public RayoNode storeNode(RayoNode node) throws DatastoreException { return delegateStore.storeNode(node); } public RayoNode updateNode(RayoNode node) throws DatastoreException { return delegateStore.updateNode(node); } public boolean equals(Object obj) { return delegateStore.equals(obj); } public RayoNode removeNode(String id) throws DatastoreException { return delegateStore.removeNode(id); } public GatewayCall storeCall(GatewayCall call) throws DatastoreException { return delegateStore.storeCall(call); } public GatewayCall removeCall(String id) throws DatastoreException { return delegateStore.removeCall(id); } public Collection<String> getCalls(String jid) { return delegateStore.getCalls(jid); } public GatewayClient storeClient(GatewayClient client) throws DatastoreException { return delegateStore.storeClient(client); } public GatewayClient removeClient(String jid) throws DatastoreException { return delegateStore.removeClient(jid); } public String toString() { return delegateStore.toString(); } public Application storeApplication(Application application) throws DatastoreException { return delegateStore.storeApplication(application); } public Application updateApplication(Application application) throws DatastoreException { return delegateStore.updateApplication(application); } public Application removeApplication(String jid) throws DatastoreException { return removeApplication(jid, true); } public Application removeApplication(String jid, boolean save) throws DatastoreException { Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); try { lock.lock(); for(String address: getAddressesForApplication(jid)) { removeAddress(address); } return delegateStore.removeApplication(jid); } finally { try { try { if (save) { write(); } } catch (IOException e) { throw new DatastoreException(e); } } finally { lock.unlock(); } } } public void storeAddress(String address, String jid) throws DatastoreException { storeAddress(address, jid, true); } public void storeAddress(String address, String jid, boolean save) throws DatastoreException { Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); try { lock.lock(); addPattern(address, jid); delegateStore.storeAddress(address, jid); } finally { try { try { if (save) { write(); } } catch (IOException e) { throw new DatastoreException(e); } } finally { lock.unlock(); } } } public void storeAddresses(Collection<String> addresses, String jid) throws DatastoreException { storeAddresses(addresses, jid, true); } public void storeAddresses(Collection<String> addresses, String jid, boolean save) throws DatastoreException { Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); try { lock.lock(); for(String address: addresses) { addPattern(address, jid); } delegateStore.storeAddresses(addresses, jid); } finally { try { try { if (save) { write(); } } catch (IOException e) { throw new DatastoreException(e); } } finally { lock.unlock(); } } } private void addPattern(String address, String jid) { if (map.get(address) == null) { PropertiesValue entry = new PropertiesValue(address,jid); map.put(address, entry); } } public void removeAddress(String address) throws DatastoreException { removeAddress(address, true); } public void removeAddress(String address, boolean save) throws DatastoreException { Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); try { lock.lock(); map.remove(address); delegateStore.removeAddress(address); } finally { try { try { if (save) { write(); } } catch (IOException e) { throw new DatastoreException(e); } } finally { lock.unlock(); } } } public GatewayMixer removeMixer(String mixerName) throws DatastoreException { return delegateStore.removeMixer(mixerName); } public GatewayMixer storeMixer(GatewayMixer mixer) throws DatastoreException { return delegateStore.storeMixer(mixer); } public void removeCallFromMixer(String callId, String mixerName) throws DatastoreException { delegateStore.removeCallFromMixer(callId, mixerName); } public void removeVerbFromMixer(String verbId, String mixerName) throws DatastoreException { delegateStore.removeVerbFromMixer(verbId, mixerName); } public List<GatewayVerb> getVerbs(String mixerName) { return delegateStore.getVerbs(mixerName); } public void removeFilter(String jid, String id) throws DatastoreException { delegateStore.removeFilter(jid, id); } public void removeFilters(String id) throws DatastoreException { delegateStore.removeFilters(id); } void write() throws IOException { File file = null; try { file = properties.getFile(); if (file == null) { logger.error("Cannot update resource state. Resource is not a file"); return; } } catch (IOException e) { logger.error("Cannot update resource state. " + e.getMessage()); return; } logger.debug("Updating properties data store: " + file.getAbsolutePath()); Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); Writer writer = null; try { lock.lock(); writer = new BufferedWriter(new FileWriter(file)); PropertiesValue global = null; for (String key: map.keySet()) { PropertiesValue entry = map.get(key); if (entry.getPattern().toString().startsWith(".*=")) { // skip global global = entry; continue; } writer.write(entry.getAddress() + "=" + entry.getApplication() + "\n"); } if (global != null) { writer.write(global + "\n"); } writer.flush(); } catch (IOException e) { logger.error(e.getMessage(),e); throw e; } finally { if (writer != null) { try { writer.close(); } catch (Exception e) { logger.error(e.getMessage(),e); } } lock.unlock(); } } void read(Resource properties) throws IOException { try { logger.debug("Reading JID Lookup Service configuration from disk [%s]", properties.getFilename()); } catch (IllegalStateException ise) { // Ignore. On testing a byte array does not have a filename property and throws an exception } Lock lock = PropertiesBasedDatastore.this.lock.writeLock(); try { lock.lock(); map.clear(); if (properties.isReadable()) { InputStream is = null; try { File file = properties.getFile(); if (file.exists()) { is = new FileInputStream(file); } } catch (IOException e) { is = properties.getInputStream(); } Scanner scanner = new Scanner(is); while(scanner.hasNextLine()) { String line = scanner.nextLine(); if (line.trim().length() > 0 && !line.trim().startsWith("#")) { String[] elements = line.trim().split("="); if (!(elements.length == 2)) { logger.error("Could not parse line %s", line); continue; } String address = elements[0].trim(); String appJid = elements[1].trim(); // create dummy application Application application = getApplication(appJid); if (application == null) { application = new Application(elements[1].trim()); storeApplication(application); } storeAddress(address, elements[1].trim(), false); } } // Finally, erase any addresses from applications that are not mapped any more List<Application> applications = getApplications(); for(Application application: applications) { List<String> addresses = getAddressesForApplication(application.getBareJid()); for(String address: addresses) { PropertiesValue entry = map.get(address); if (entry == null) { logger.debug("Removing mapping for pattern [%s]", address); removeAddress(address, false); } } } } else { logger.warn("Could not find JID lookup service configuration file [%s]", properties); } } catch (Exception e) { logger.error(e.getMessage(),e); throw new IOException(e); } finally { lock.unlock(); } } /** * Returns an application jid for the given address URI. It will go through all the * different regexp expressions associated with the application and get the first one * * @param uri Address * @return String JId of the application that matches the address */ public String lookup(URI uri) { Lock lock = this.lock.readLock(); try { lock.lock(); for(String key: map.keySet()) { PropertiesValue entry = map.get(key); Matcher matcher = entry.getPattern().matcher(uri.toString()); if (matcher.matches()) { String value = entry.getApplication(); if (logger.isDebugEnabled()) { logger.debug("Found a match for %s : %s", uri.toString(), value); } return value; } } if (logger.isDebugEnabled()) { logger.debug("We didn't find any Regexp match for %s", uri.toString()); } return null; } finally { lock.unlock(); } } int getLoadFailures() { return failures; } }