/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.hawkular.inventory.impl.tinkerpop.provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Transaction;
import org.hawkular.inventory.api.Configuration;
import org.hawkular.inventory.api.EntityAlreadyExistsException;
import org.hawkular.inventory.impl.tinkerpop.spi.GraphProvider;
import org.hawkular.inventory.impl.tinkerpop.spi.IndexSpec;
import org.hawkular.inventory.paths.CanonicalPath;
import com.thinkaurelius.titan.core.PropertyKey;
import com.thinkaurelius.titan.core.SchemaViolationException;
import com.thinkaurelius.titan.core.TitanException;
import com.thinkaurelius.titan.core.TitanFactory;
import com.thinkaurelius.titan.core.TitanGraph;
import com.thinkaurelius.titan.core.schema.PropertyKeyMaker;
import com.thinkaurelius.titan.core.schema.TitanManagement;
/**
* @author Lukas Krejci
* @since 0.0.1
*/
public class TitanProvider implements GraphProvider {
private static class ExceptionMapper {
private Class<? extends RuntimeException> targetException;
private Predicate<RuntimeException> predicate;
public ExceptionMapper(Class<? extends RuntimeException> targetException,
Predicate<RuntimeException> predicate) {
this.targetException = targetException;
this.predicate = predicate;
}
public Class<? extends RuntimeException> getTargetException() {
return targetException;
}
public Predicate<RuntimeException> getPredicate() {
return predicate;
}
}
private static final Map<Class<? extends RuntimeException>, List<ExceptionMapper>> exceptionMapping =
new HashMap<>();
private static void mapException(Class<? extends RuntimeException> source, Class<? extends RuntimeException> target,
Predicate<RuntimeException> p) {
if (exceptionMapping.get(source) == null) {
exceptionMapping.put(source, new ArrayList<>());
}
exceptionMapping.get(source).add(new ExceptionMapper(target, p));
}
static {
try {
mapException(SchemaViolationException.class, EntityAlreadyExistsException.class,
e -> e.getMessage().contains("violates a uniqueness constraint [by___cp]"));
} catch (Throwable t) {
// never fail during the class loading
}
}
@Override public boolean isPreferringBigTransactions() {
return false;
}
@Override public boolean isUniqueIndexSupported() {
return false;
}
@Override public boolean needsDraining() {
return false;
}
@Override
public TitanGraph instantiateGraph(Configuration configuration) {
TitanGraph g = TitanFactory.open(new MapConfiguration(configuration.prefixedWith(ALLOWED_PREFIXES)
.getImplementationConfiguration(EnumSet.allOf(PropertyKeys.class))));
g.tx().onReadWrite(Transaction.READ_WRITE_BEHAVIOR.MANUAL);
return g;
}
@Override
public void ensureIndices(Graph graph, IndexSpec... indexSpecs) {
Set<IndexSpec.Property> undefinedPropertyKeys = new HashSet<>();
Map<String, PropertyKey> definedPropertyKeys = new HashMap<>();
Map<String, IndexSpec> undefinedIndices = new HashMap<>();
TitanManagement mgmt = ((TitanGraph) graph).openManagement();
for (IndexSpec spec : indexSpecs) {
String indexName = getIndexName(spec.getProperties());
if (mgmt.getGraphIndex(indexName) == null) {
undefinedIndices.put(indexName, spec);
}
//the indices might share keys, so we need to check for the keys even if the index doesn't exist
for (IndexSpec.Property p : spec.getProperties()) {
PropertyKey key = mgmt.getPropertyKey(p.getName());
if (key == null) {
undefinedPropertyKeys.add(p);
} else {
if (!key.dataType().equals(p.getType())) {
throw new IllegalStateException("There already is a key '" + key.name() +
"' that would be needed for index " + spec + ". The key has a different data type" +
" than expected, though. Expected: '" + p.getType() + "', actual: '" +
key.dataType() + "'.");
}
definedPropertyKeys.put(p.getName(), key);
}
}
}
//first define all the undefined property keys
for (IndexSpec.Property p : undefinedPropertyKeys) {
PropertyKeyMaker propertyKeyMaker = mgmt.makePropertyKey(p.getName()).dataType(p.getType());
//this is currently not used and not working with Titan 1.0
// if (null != p.getLabelIndex()) {
// propertyKeyMaker.signature(mgmt.makeEdgeLabel(p.getLabelIndex())
// // index on directed edges doesn't work
// .unidirected()
// .multiplicity(Multiplicity.SIMPLE)
// .make());
// }
// XXX just don't create unique indices even if Titan supports it. They are not performant enough in concurrent
// scenarios
//
if (p.isUnique()) {
Log.LOG.wUniqueIndicesIgnored();
//propertyKeyMaker.cardinality(Cardinality.SINGLE);
}
PropertyKey key = propertyKeyMaker.make();
definedPropertyKeys.put(p.getName(), key);
}
for (Map.Entry<String, IndexSpec> e : undefinedIndices.entrySet()) {
TitanManagement.IndexBuilder bld = mgmt.buildIndex(e.getKey(), e.getValue().getElementType());
if (e.getValue().isUnique()) {
bld.unique();
}
for (IndexSpec.Property k : e.getValue().getProperties()) {
// bld.addKey(definedPropertyKeys.get(k.getName()), Parameter.of("mapped-name", k.getName()));
bld.addKey(definedPropertyKeys.get(k.getName()));
}
bld.buildCompositeIndex();
}
mgmt.commit();
}
private String getIndexName(Set<IndexSpec.Property> properties) {
StringBuilder bld = new StringBuilder("by");
for (IndexSpec.Property property : properties) {
bld.append("_").append(property.getName());
}
return bld.toString();
}
public static final String[] ALLOWED_PREFIXES = {"attributes", "cache", "cluster", "graph", "ids", "index", "log" +
"metrics", "query", "schema", "storage", "tx"};
@SuppressWarnings("unused")
private enum PropertyKeys implements Configuration.Property {
STORAGE_HOSTNAME("storage.hostname", "hawkular.inventory.titan.storage.hostname",
"HAWKULAR_INVENTORY_TITAN_STORAGE_HOSTNAME", "CASSANDRA_NODES"),
STORAGE_PORT("storage.port", "hawkular.inventory.titan.storage.port", "HAWKULAR_INVENTORY_TITAN_STORAGE_PORT"),
STORAGE_CASSANDRA_KEYSPACE("storage.cassandra.keyspace", "hawkular.inventory.titan.storage.cassandra.keyspace",
"HAWKULAR_INVENTORY_TITAN_STORAGE_CASSANDRA_KEYSPACE");
private final String propertyName;
private final List<String> systemPropertyName;
private final List<String> environmentVariableName;
PropertyKeys(String propertyName, String systemPropertyName, String... environmentVariableName) {
this.propertyName = propertyName;
this.systemPropertyName = Collections.unmodifiableList(Collections.singletonList(systemPropertyName));
this.environmentVariableName = Collections.unmodifiableList(Arrays.asList(environmentVariableName));
}
@Override
public String getPropertyName() {
return propertyName;
}
@Override
public List<String> getSystemPropertyNames() {
return systemPropertyName;
}
@Override
public List<String> getEnvironmentVariableNames() {
return environmentVariableName;
}
}
@Override
public RuntimeException translateException(RuntimeException inputException, CanonicalPath affectedPath) {
List<ExceptionMapper> exceptionMappers = exceptionMapping.get(inputException.getClass());
if (exceptionMappers != null) {
Optional<RuntimeException> firstMatch =
exceptionMappers.stream()
.filter(mapper -> mapper.getPredicate().test(inputException))
.findFirst()
.map(mapper -> {
try {
// todo: find the proper ctor based on parameter match
// Arrays.stream(mapper.getTargetException().getConstructors()).forEach(
// constructor -> {
//
// Class<?>[] params = constructor.getParameterTypes();
// }
// );
return mapper.getTargetException().getConstructor(String.class).newInstance
(inputException.getMessage());
} catch (Exception e) {
return inputException;
}
});
if (firstMatch.isPresent()) {
return firstMatch.orElseGet(() -> inputException);
}
}
return inputException;
}
@Override public boolean requiresRollbackAfterFailure(Throwable t) {
while (t != null) {
if (t instanceof TitanException
&& "Could not commit transaction due to exception during persistence".equals(t.getMessage())) {
return false;
}
t = t.getCause();
}
return true;
}
}