package com.laytonsmith.persistence; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.DaemonManager; import com.laytonsmith.annotations.datasource; import com.laytonsmith.persistence.io.ConnectionMixin; import com.laytonsmith.persistence.io.ConnectionMixinFactory; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; /** * */ public abstract class AbstractDataSource implements DataSource { protected final URI uri; protected final Set<DataSourceModifier> modifiers = EnumSet.noneOf(DataSourceModifier.class); private Set<DataSourceModifier> invalidModifiers; private ConnectionMixin connectionMixin; private ConnectionMixinFactory.ConnectionMixinOptions mixinOptions; private boolean inTransaction = false; protected AbstractDataSource() { try { uri = new URI(""); } catch (URISyntaxException ex) { throw new RuntimeException(ex); } } protected AbstractDataSource(URI uri, ConnectionMixinFactory.ConnectionMixinOptions mixinOptions) throws DataSourceException { this.uri = uri; this.mixinOptions = mixinOptions; setInvalidModifiers(); DataSourceModifier[] implicit = this.implicitModifiers(); if (implicit != null) { for (DataSourceModifier dsm : this.implicitModifiers()) { addModifier(dsm); } } } /** * Gets the connection mixin for this connection type. * @return * @throws DataSourceException */ protected ConnectionMixin getConnectionMixin() throws DataSourceException{ if(connectionMixin == null){ connectionMixin = ConnectionMixinFactory.GetConnectionMixin(uri, modifiers, mixinOptions, getBlankDataModel()); } return connectionMixin; } /** * {@inheritDoc} */ @Override public final String get(String[] key) throws DataSourceException { checkGet(key); return get0(key); } @Override public final Map<String[], String> getValues(String[] leadKey) throws DataSourceException { checkGet(leadKey); return getValues0(leadKey); } /** * By default, we use the naive method to get the values, by getting the keys in step 1, then * performing x gets to retrieve the values. This can probably be optimized * to reduce the number of get calls in some data sources, and should be overridden if so. */ protected Map<String[], String> getValues0(String[] leadKey) throws DataSourceException { Map<String[], String> map = new HashMap<>(); for(String [] key : getNamespace(leadKey)){ map.put(key, get(key)); } return map; } @Override public final void startTransaction(DaemonManager dm) { inTransaction = true; startTransaction0(dm); } @Override public final void stopTransaction(DaemonManager dm, boolean rollback) throws DataSourceException, IOException { inTransaction = false; stopTransaction0(dm, rollback); } /** * Returns true if we are currently in a transaction. Inside of the call to * stopTransaction, this will be false. * @return */ public boolean inTransaction(){ return inTransaction; } protected abstract void startTransaction0(DaemonManager dm); protected abstract void stopTransaction0(DaemonManager dm, boolean rollback) throws DataSourceException, IOException; @Override public final boolean set(DaemonManager dm, String[] key, String value) throws ReadOnlyException, DataSourceException, IOException { checkSet(key); return set0(dm, key, value); } /** * Subclasses should implement this, instead of set(), as our version of set() does some standard validation * on the input. * @param key * @param value * @return * @throws ReadOnlyException * @throws DataSourceException * @throws IOException */ protected abstract boolean set0(DaemonManager dm, String[] key, String value) throws ReadOnlyException, DataSourceException, IOException; /** * Subclasses should implement this, instead of get(), as our version of get() does * some standard validation on the input. * @param key * @return */ protected abstract String get0(String[] key) throws DataSourceException; /** * The default implementation of string simply walks through keySet, and * manually joins the keys together. If an implementation can provide a * more efficient method, this should be overridden. * * @return */ @Override public Set<String> stringKeySet(String [] keyBase) throws DataSourceException { Set<String> keys = new TreeSet<String>(); for (String[] key : keySet(keyBase)) { keys.add(StringUtils.Join(key, ".")); } return keys; } @Override public Set<String[]> getNamespace(String[] namespace) throws DataSourceException { Set<String[]> list = new HashSet<String[]>(); String ns = StringUtils.Join(namespace, "."); for (String key : stringKeySet(namespace)) { if ("".equals(ns) //Blank string; this means they want it to always match. || key.matches(Pattern.quote(ns) + "(?:$|\\..*)")) { String[] split = key.split("\\."); list.add(split); } } return list; } private void setInvalidModifiers() { DataSourceModifier[] invalid = this.invalidModifiers(); if (invalid == null) { return; } this.invalidModifiers = EnumSet.copyOf(Arrays.asList(invalid)); } @Override public final String getName() { return this.getClass().getAnnotation(datasource.class).value(); } @Override public final void addModifier(DataSourceModifier modifier) { if (invalidModifiers != null && invalidModifiers.contains(modifier)) { return; } if (modifier == DataSourceModifier.HTTP || modifier == DataSourceModifier.HTTPS) { modifiers.add(DataSourceModifier.READONLY); modifiers.add(DataSourceModifier.ASYNC); } if (modifier == DataSourceModifier.SSH){ modifiers.add(DataSourceModifier.ASYNC); } modifiers.add(modifier); } @Override public final boolean hasKey(String[] key) throws DataSourceException { checkGet(key); return hasKey0(key); } /** * By default, returns true if the value stored is non-null. In general, * if clearKey0 is overridden, this should be as well. * @param key * @return * @throws DataSourceException */ protected boolean hasKey0(String[] key) throws DataSourceException{ return get(key) != null; } @Override public final void clearKey(DaemonManager dm, String [] key) throws ReadOnlyException, DataSourceException, IOException{ checkSet(key); clearKey0(dm, key); } /** * By default, setting the value to null should clear the value, * but that can be overridden if a data source has a better method. * @param key * @throws ReadOnlyException * @throws DataSourceException * @throws IOException */ protected void clearKey0(DaemonManager dm, String [] key) throws ReadOnlyException, DataSourceException, IOException{ set(dm, key, null); } /** * This method checks for invalid or non-sensical combinations of * modifiers, and throws an exception if any combinations exist that are * strange. * * @throws DataSourceException */ public final void checkModifiers() throws DataSourceException { List<String> errors = new ArrayList(); if (invalidModifiers != null) { for (DataSourceModifier dsm : invalidModifiers) { if (modifiers.contains(dsm)) { errors.add(uri.toString() + " contains the modifier " + dsm.getName() + ", which is not applicable. This will be ignored."); } } } if (modifiers.contains(DataSourceModifier.PRETTYPRINT) && modifiers.contains(DataSourceModifier.READONLY)) { errors.add(uri.toString() + " contains both prettyprint and readonly modifiers, which doesn't make sense, because we cannot write out the file; prettyprint will be ignored."); modifiers.remove(DataSourceModifier.PRETTYPRINT); } if ((modifiers.contains(DataSourceModifier.HTTP) || modifiers.contains(DataSourceModifier.HTTPS)) && modifiers.contains(DataSourceModifier.SSH)) { errors.add(uri.toString() + " contains both http(s) and ssh modifiers."); } if (modifiers.contains(DataSourceModifier.HTTP) && modifiers.contains(DataSourceModifier.HTTPS)) { errors.add(uri.toString() + " contains both http and https modifiers. Because these are mutually exclusive, this doesn't make sense, and https will be assumed."); modifiers.remove(DataSourceModifier.HTTP); } if (!errors.isEmpty()) { throw new DataSourceException(StringUtils.Join(errors, "\n")); } } @Override public final boolean hasModifier(DataSourceModifier modifier) { return modifiers.contains(modifier); } /** * This method checks to see if a set operation should simply throw a * ReadOnlyException based on the modifiers. */ private void checkSet(String [] key) throws ReadOnlyException { for(String namespace : key){ if("_".equals(namespace)){ throw new IllegalArgumentException("In the key \"" + StringUtils.Join(key, ".") + ", the namespace \"_\" is not allowed." + " (Namespaces may contain an underscore, but may not be just an underscore.)"); } } if (modifiers.contains(DataSourceModifier.READONLY)) { throw new ReadOnlyException(); } } /** * This method checks to see if get operations should re-populate at * this time. If the data set is transient, it will do so. */ private void checkGet(String[] key) throws DataSourceException { for(String namespace : key){ if("_".equals(namespace)){ throw new IllegalArgumentException("In the key \"" + StringUtils.Join(key, ".") + ", the namespace \"_\" is not allowed." + " (Namespaces may contain an underscore, but may not be just an underscore.)"); } } if(this.getModifiers().contains(DataSource.DataSourceModifier.TRANSIENT)){ this.populate(); } if (hasModifier(DataSourceModifier.TRANSIENT)) { populate(); } } @Override public final Set<DataSourceModifier> getModifiers() { return EnumSet.copyOf(modifiers); } /** * Subclasses that need a certain type of file to be the "blank" version * of a data model can override this. By default, null is * returned. * * @return */ protected String getBlankDataModel() { return ""; } @Override public String toString(){ StringBuilder b = new StringBuilder(); for(DataSourceModifier m : modifiers){ b.append(m.getName().toLowerCase()).append(":"); } b.append(uri.toString()); return b.toString(); } }