/* * Copyright © 2015 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package co.cask.cdap.config; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.dataset.DatasetDefinition; import co.cask.cdap.api.dataset.DatasetManagementException; import co.cask.cdap.api.dataset.DatasetProperties; import co.cask.cdap.api.dataset.table.Row; import co.cask.cdap.api.dataset.table.Scanner; import co.cask.cdap.api.dataset.table.Table; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.data2.datafabric.dataset.DatasetsUtil; import co.cask.cdap.data2.dataset2.DatasetFramework; import co.cask.cdap.data2.dataset2.tx.Transactional; import co.cask.cdap.proto.Id; import co.cask.tephra.TransactionExecutor; import co.cask.tephra.TransactionExecutorFactory; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.reflect.Type; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Default Configuration Store. */ public class DefaultConfigStore implements ConfigStore { private static final Logger LOG = LoggerFactory.getLogger(DefaultConfigStore.class); private static final Gson GSON = new Gson(); private static final Type MAP_STRING_STRING_TYPE = new TypeToken<Map<String, String>>() { }.getType(); private static final String PROPERTY_COLUMN = "properties"; private static final Id.DatasetInstance CONFIG_STORE_DATASET_INSTANCE_ID = Id.DatasetInstance.from(Id.Namespace.SYSTEM, Constants.ConfigStore.CONFIG_TABLE); private final Transactional<ConfigTable, Table> txnl; @Inject public DefaultConfigStore(CConfiguration cConf, final DatasetFramework datasetFramework, TransactionExecutorFactory executorFactory) { txnl = Transactional.of(executorFactory, new Supplier<ConfigTable>() { @Override public ConfigTable get() { try { Table table = DatasetsUtil.getOrCreateDataset(datasetFramework, CONFIG_STORE_DATASET_INSTANCE_ID, "table", DatasetProperties.EMPTY, DatasetDefinition.NO_ARGUMENTS, null); return new ConfigTable(table); } catch (Exception e) { LOG.error("Failed to access {} table", Constants.ConfigStore.CONFIG_TABLE, e); throw Throwables.propagate(e); } } }); } public static void setupDatasets(DatasetFramework dsFramework) throws DatasetManagementException, IOException { dsFramework.addInstance(Table.class.getName(), Id.DatasetInstance.from(Id.Namespace.SYSTEM, Constants.ConfigStore.CONFIG_TABLE), DatasetProperties.EMPTY); } @Override public void create(final String namespace, final String type, final Config config) throws ConfigExistsException { Boolean success = txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, Boolean>() { @Override public Boolean apply(ConfigTable configTable) throws Exception { if (!configTable.table.get(rowKey(namespace, type, config.getId())).isEmpty()) { return false; } configTable.table.put(rowKey(namespace, type, config.getId()), Bytes.toBytes(PROPERTY_COLUMN), Bytes.toBytes(GSON.toJson(config.getProperties()))); return true; } }); if (!success) { throw new ConfigExistsException(namespace, type, config.getId()); } } @Override public void createOrUpdate(final String namespace, final String type, final Config config) { txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, Void>() { @Override public Void apply(ConfigTable configTable) throws Exception { configTable.table.put(rowKey(namespace, type, config.getId()), Bytes.toBytes(PROPERTY_COLUMN), Bytes.toBytes(GSON.toJson(config.getProperties()))); return null; } }); } @Override public void delete(final String namespace, final String type, final String id) throws ConfigNotFoundException { Boolean success = txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, Boolean>() { @Override public Boolean apply(ConfigTable configTable) throws Exception { if (configTable.table.get(rowKey(namespace, type, id)).isEmpty()) { return false; } configTable.table.delete(rowKey(namespace, type, id)); return true; } }); if (!success) { throw new ConfigNotFoundException(namespace, type, id); } } @Override public List<Config> list(final String namespace, final String type) { return txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, List<Config>>() { @Override public List<Config> apply(ConfigTable configTable) throws Exception { List<Config> configList = Lists.newArrayList(); byte[] prefixBytes = rowKeyPrefix(namespace, type); Scanner rows = configTable.table.scan(prefixBytes, Bytes.stopKeyForPrefix(prefixBytes)); Row row; while ((row = rows.next()) != null) { Map<String, String> properties = GSON.fromJson(Bytes.toString(row.get(Bytes.toBytes(PROPERTY_COLUMN))), MAP_STRING_STRING_TYPE); configList.add(new Config(getPart(row.getRow(), prefixBytes.length), properties)); } return configList; } }); } @Override public Config get(final String namespace, final String type, final String id) throws ConfigNotFoundException { Config config = txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, Config>() { @Override public Config apply(ConfigTable configTable) throws Exception { Row row = configTable.table.get(rowKey(namespace, type, id)); if (row.isEmpty()) { return null; } Map<String, String> propertyMap = GSON.fromJson(Bytes.toString(row.get(Bytes.toBytes(PROPERTY_COLUMN))), MAP_STRING_STRING_TYPE); return new Config(id, propertyMap); } }); if (config == null) { throw new ConfigNotFoundException(namespace, type, id); } else { return config; } } @Override public void update(final String namespace, final String type, final Config config) throws ConfigNotFoundException { Boolean success = txnl.executeUnchecked(new TransactionExecutor.Function<ConfigTable, Boolean>() { @Override public Boolean apply(ConfigTable configTable) throws Exception { if (configTable.table.get(rowKey(namespace, type, config.getId())).isEmpty()) { return false; } configTable.table.put(rowKey(namespace, type, config.getId()), Bytes.toBytes(PROPERTY_COLUMN), Bytes.toBytes(GSON.toJson(config.getProperties()))); return true; } }); if (!success) { throw new ConfigNotFoundException(namespace, type, config.getId()); } } private byte[] rowKey(String namespace, String type, String id) { return getMultipartKey(namespace, type, id); } private byte[] rowKeyPrefix(String namespace, String type) { return getMultipartKey(namespace, type); } private String getPart(byte[] rowBytes, int offset) { int length = Bytes.toInt(rowBytes, offset, Bytes.SIZEOF_INT); return Bytes.toString(rowBytes, offset + Bytes.SIZEOF_INT, length); } private byte[] getMultipartKey(String... parts) { int sizeOfParts = 0; for (String part : parts) { sizeOfParts += part.length(); } byte[] result = new byte[1 + sizeOfParts + (parts.length * Bytes.SIZEOF_INT)]; Bytes.putByte(result, 0, Constants.ConfigStore.VERSION); int offset = 1; for (String part : parts) { Bytes.putInt(result, offset, part.length()); offset += Bytes.SIZEOF_INT; Bytes.putBytes(result, offset, part.getBytes(), 0, part.length()); offset += part.length(); } return result; } private static final class ConfigTable implements Iterable<Table> { private final Table table; private ConfigTable(Table table) { this.table = table; } @Override public Iterator<Table> iterator() { return Iterators.singletonIterator(table); } } }