/** * Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT * All rights reserved. Use is subject to license terms. See LICENSE.TXT */ package org.diirt.datasource; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * A data source that can dispatch a request to multiple different * data sources. * * @author carcassi */ public class CompositeDataSource extends DataSource { private static final Logger log = Logger.getLogger(CompositeDataSource.class.getName()); // Stores all data sources by name private final Map<String, DataSource> dataSources = new ConcurrentHashMap<>(); private final Map<String, DataSourceProvider> dataSourceProviders = new ConcurrentHashMap<>(); private volatile CompositeDataSourceConfiguration conf = new CompositeDataSourceConfiguration(); /** * Creates a new CompositeDataSource. */ public CompositeDataSource() { super(true); } /** * The configuration used for the composite data source. * * @return the configuration; can't be null */ public CompositeDataSourceConfiguration getConfiguration() { return conf; } /** * Changes the composite data source configuration. * <p> * NOTE: the configuration should be changed before any channel * is opened. The result of later changes is not well defined. * * @param conf the new configuration; can't be null */ public void setConfiguration(CompositeDataSourceConfiguration conf) { if (conf == null) { throw new NullPointerException("Configuration can't be null."); } this.conf = conf; } /** * Adds/replaces the data source corresponding to the given name. * * @param name the name of the data source * @param dataSource the data source to add/replace */ public void putDataSource(final String name, final DataSource dataSource) { putDataSource(new DataSourceProvider() { @Override public String getName() { return name; } @Override public DataSource createInstance() { return dataSource; } }); } /** * Adds/replaces the data source corresponding to the given name. * * @param dataSourceProvider the data source to add/replace */ public void putDataSource(DataSourceProvider dataSourceProvider) { // XXX: datasources should be closed dataSources.remove(dataSourceProvider.getName()); dataSourceProviders.put(dataSourceProvider.getName(), dataSourceProvider); } /** * Returns the data sources used by this composite data source. * <p> * Returns only the data sources that have been created. * * @return the registered data sources */ public Map<String, DataSource> getDataSources() { return Collections.unmodifiableMap(dataSources); } /** * Returns the data source providers registered to this composite data source. * <p> * Returns all registered data sources. * * @return the registered data source providers */ public Map<String, DataSourceProvider> getDataSourceProviders() { return Collections.unmodifiableMap(dataSourceProviders); } private String nameOf(String channelName) { String delimiter = conf.delimiter; int indexDelimiter = channelName.indexOf(delimiter); if (indexDelimiter == -1) { return channelName; } else { return channelName.substring(indexDelimiter + delimiter.length()); } } private String sourceOf(String channelName) { String delimiter = conf.delimiter; String defaultDataSource = conf.defaultDataSource; int indexDelimiter = channelName.indexOf(delimiter); if (indexDelimiter == -1) { if (defaultDataSource == null) throw new IllegalArgumentException("Channel " + channelName + " uses default data source but one was never set."); if (!dataSourceProviders.containsKey(defaultDataSource)) throw new IllegalArgumentException("Channel " + channelName + " uses default data source " + defaultDataSource + " which was not found."); return defaultDataSource; } else { String source = channelName.substring(0, indexDelimiter); if (dataSourceProviders.containsKey(source)) return source; throw new IllegalArgumentException("Data source " + source + " for " + channelName + " was not configured."); } } private Map<String, ReadRecipe> splitRecipe(ReadRecipe readRecipe) { Map<String, ReadRecipe> splitRecipe = new HashMap<String, ReadRecipe>(); // Iterate through the recipe to understand how to distribute // the calls Map<String, Collection<ChannelReadRecipe>> routingRecipes = new HashMap<String, Collection<ChannelReadRecipe>>(); for (ChannelReadRecipe channelRecipe : readRecipe.getChannelReadRecipes()) { String name = nameOf(channelRecipe.getChannelName()); String dataSource = sourceOf(channelRecipe.getChannelName()); if (dataSource == null) throw new IllegalArgumentException("Channel " + name + " uses the default data source but one was never set."); // Add recipe for the target dataSource if (routingRecipes.get(dataSource) == null) { routingRecipes.put(dataSource, new HashSet<ChannelReadRecipe>()); } routingRecipes.get(dataSource).add(new ChannelReadRecipe(name, channelRecipe.getReadSubscription())); } // Create the recipes for (Entry<String, Collection<ChannelReadRecipe>> entry : routingRecipes.entrySet()) { splitRecipe.put(entry.getKey(), new ReadRecipe(entry.getValue())); } return splitRecipe; } @Override public void connectRead(ReadRecipe readRecipe) { Map<String, ReadRecipe> splitRecipe = splitRecipe(readRecipe); // Dispatch calls to all the data sources for (Map.Entry<String, ReadRecipe> entry : splitRecipe.entrySet()) { try { retrieveDataSource(entry.getKey()).connectRead(entry.getValue()); } catch (RuntimeException ex) { // If data source fail, still go and connect the others readRecipe.getChannelReadRecipes().iterator().next().getReadSubscription().getExceptionWriteFunction().writeValue(ex); } } } @Override public void disconnectRead(ReadRecipe readRecipe) { Map<String, ReadRecipe> splitRecipe = splitRecipe(readRecipe); // Dispatch calls to all the data sources for (Map.Entry<String, ReadRecipe> entry : splitRecipe.entrySet()) { try { dataSources.get(entry.getKey()).disconnectRead(entry.getValue()); } catch(RuntimeException ex) { // If a data source fails, still go and disconnect the others readRecipe.getChannelReadRecipes().iterator().next().getReadSubscription().getExceptionWriteFunction().writeValue(ex); } } } private Map<String, WriteRecipe> splitRecipe(WriteRecipe writeRecipe) { // Chop the recipe along different data sources Map<String, Collection<ChannelWriteRecipe>> recipes = new HashMap<String, Collection<ChannelWriteRecipe>>(); for (ChannelWriteRecipe channelWriteRecipe : writeRecipe.getChannelWriteRecipes()) { String channelName = nameOf(channelWriteRecipe.getChannelName()); String dataSource = sourceOf(channelWriteRecipe.getChannelName()); Collection<ChannelWriteRecipe> channelWriteRecipes = recipes.get(dataSource); if (channelWriteRecipes == null) { channelWriteRecipes = new ArrayList<ChannelWriteRecipe>(); recipes.put(dataSource, channelWriteRecipes); } channelWriteRecipes.add(new ChannelWriteRecipe(channelName, channelWriteRecipe.getWriteSubscription())); } Map<String, WriteRecipe> splitRecipes = new HashMap<String, WriteRecipe>(); for (Map.Entry<String, Collection<ChannelWriteRecipe>> en : recipes.entrySet()) { String dataSource = en.getKey(); Collection<ChannelWriteRecipe> val = en.getValue(); WriteRecipe newWriteRecipe = new WriteRecipe(val); splitRecipes.put(dataSource, newWriteRecipe); } return splitRecipes; } private DataSource retrieveDataSource(String name) { DataSource dataSource = dataSources.get(name); if (dataSource == null) { DataSourceProvider factory = dataSourceProviders.get(name); if (factory == null) { throw new IllegalArgumentException("DataSource '" + name + conf.delimiter + "' was not configured."); } else { dataSource = factory.createInstance(); if (dataSource == null) { throw new IllegalStateException("DataSourceProvider '" + name + conf.delimiter + "' did not create a valid datasource."); } dataSources.put(name, dataSource); log.log(Level.CONFIG, "Created instance for data source {0} ({1})", new Object[]{name, dataSource.getClass().getSimpleName()}); } } return dataSource; } @Override public void connectWrite(WriteRecipe writeRecipe) { Map<String, WriteRecipe> splitRecipes = splitRecipe(writeRecipe); for (Entry<String, WriteRecipe> entry : splitRecipes.entrySet()) { String dataSource = entry.getKey(); WriteRecipe splitWriteRecipe = entry.getValue(); retrieveDataSource(dataSource).connectWrite(splitWriteRecipe); } } @Override public void disconnectWrite(WriteRecipe writeRecipe) { Map<String, WriteRecipe> splitRecipe = splitRecipe(writeRecipe); for (Map.Entry<String, WriteRecipe> en : splitRecipe.entrySet()) { String dataSource = en.getKey(); WriteRecipe splitWriteRecipe = en.getValue(); dataSources.get(dataSource).disconnectWrite(splitWriteRecipe); } } @Override ChannelHandler channel(String channelName) { String name = nameOf(channelName); String dataSource = sourceOf(channelName); return dataSources.get(dataSource).channel(name); } @Override protected ChannelHandler createChannel(String channelName) { throw new UnsupportedOperationException("Composite data source can't create channels directly."); } /** * Closes all DataSources that are registered in the composite. */ @Override public void close() { for (DataSource dataSource : dataSources.values()) { dataSource.close(); } } @Override public Map<String, ChannelHandler> getChannels() { Map<String, ChannelHandler> channels = new HashMap<String, ChannelHandler>(); for (Entry<String, DataSource> entry : dataSources.entrySet()) { String dataSourceName = entry.getKey(); DataSource dataSource = entry.getValue(); for (Entry<String, ChannelHandler> channelEntry : dataSource.getChannels().entrySet()) { String channelName = channelEntry.getKey(); ChannelHandler channelHandler = channelEntry.getValue(); channels.put(dataSourceName + conf.delimiter + channelName, channelHandler); } } return channels; } }