package com.forter.contracts; import backtype.storm.task.TopologyContext; import backtype.storm.topology.BasicOutputCollector; import backtype.storm.topology.FailedException; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.topology.ReportedFailedException; import backtype.storm.topology.base.BaseBasicBolt; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Tuple; import backtype.storm.utils.Utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; import com.forter.contracts.cache.CacheDAO; import com.forter.contracts.cache.CacheKeyFilter; import com.forter.contracts.cache.Cached; import com.forter.contracts.cache.DummyCacheDAO; import com.forter.contracts.reflection.ContractsBoltReflector; import com.forter.contracts.validation.ContractValidator; import com.forter.contracts.validation.ValidatedContract; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import javax.validation.ValidationException; import java.io.Serializable; import java.util.*; import static com.google.common.collect.Iterables.*; /** * Bolt base class that uses Data Objects for input and output. */ public class BaseContractsBoltExecutor<TInput, TOutput, TContractsBolt extends IContractsBolt<TInput, TOutput>> extends BaseBasicBolt { private final TContractsBolt delegate; private transient ContractFactory<TInput> inputFactory; private transient ContractsBoltReflector reflector; private transient TOutput defaultOutput; private transient BaseContractsBoltExecutor.IsInvalidPredicate isInvalidPredicate; private transient BaseContractsBoltExecutor.ValidateContractTransformation validationTransformation; private transient String id; private transient CacheDAO<? super TOutput> cache; private transient CacheKeyFilter cacheKeyFilter; private transient boolean isCacheSupported; private transient boolean isEnrichmentBolt; private transient Set<String> outputFieldsToOmit; public BaseContractsBoltExecutor(TContractsBolt contractsBolt) { this.delegate = contractsBolt; } @Override public void prepare(Map stormConf, TopologyContext context) { this.reflector = new ContractsBoltReflector(this.delegate); this.validationTransformation = new ValidateContractTransformation(); this.isInvalidPredicate = new IsInvalidPredicate(); this.inputFactory = new ContractFactory(this.reflector.getInputClass()); this.delegate.prepare(stormConf, context); this.defaultOutput = this.delegate.createDefaultOutput(); this.id = context.getThisComponentId(); Preconditions.checkNotNull(this.defaultOutput, "getDefaultOutput cannot return null. Use Optional.absent() instead of null."); final ValidatedContract<TOutput> validationResult = ContractValidator.instance().validate(this.defaultOutput); Preconditions.checkState( validationResult.isValid(), "Default output failed contract validation: %s", validationResult.toString()); this.isCacheSupported = this.reflector.getInputClass().isAnnotationPresent(Cached.class); this.cache = isCacheSupported ? createCacheDAO(stormConf, context) : new DummyCacheDAO<TOutput>(); this.cacheKeyFilter = new CacheKeyFilter(this.reflector.getInputClass()); this.isEnrichmentBolt = this.delegate.getClass().isAnnotationPresent(EnrichmentBolt.class); this.outputFieldsToOmit = new HashSet<>(); if (this.isEnrichmentBolt) { EnrichmentBolt enrichmentAnnotation = this.delegate.getClass().getAnnotation(EnrichmentBolt.class); for (String field : enrichmentAnnotation.fieldsToOmit()) { outputFieldsToOmit.add(field); } } } @Override public void execute(Tuple inputTuple, BasicOutputCollector collector) { TOutput output = defaultOutput; RuntimeException exception = null; Boolean foundCache = null; final Object id = inputTuple.getValue(0); try { final Object data = inputTuple.getValue(1); ValidatedContract validatedInputContract = transformAndValidateInput(data); if (!validatedInputContract.isValid()) { handleInputError(validatedInputContract, this.id, inputTuple); return; } else { TInput input = (TInput) validatedInputContract.getContract(); Map<String, Object> cacheKeyData; if (data instanceof ObjectNode) { cacheKeyData = cacheKeyFilter.createKey((ObjectNode) data); } else { cacheKeyData = cacheKeyFilter.createKey(input); } Optional<? super TOutput> cachedOutput = this.cache.get(cacheKeyData); foundCache = cachedOutput.isPresent(); if (foundCache) { output = (TOutput) cachedOutput.get(); } else { long startTime = System.currentTimeMillis(); output = delegate.execute(input); this.cache.save(output, cacheKeyData, startTime); } } } catch (FailedException cve) { // includes ContractViolationReportedFailedException exception = cve; } catch (RuntimeException e) { exception = new ReportedFailedException(e); } finally { if (this.isCacheSupported && foundCache != null) { this.reportCacheStatus(foundCache, inputTuple); } Iterable<Object> outputContracts; Iterable<ValidatedContract> invalidOutputContracts; if (output == null) { outputContracts = ImmutableList.of(); invalidOutputContracts = ImmutableList.of(validationTransformation.apply(output)); } else { outputContracts = iterableContracts(output); invalidOutputContracts = filter(transform(outputContracts, validationTransformation), isInvalidPredicate); } if (isEmpty(invalidOutputContracts)) { for (Object contract : outputContracts) { emit(id, contract, inputTuple, collector); } } else { emit(id, defaultOutput, inputTuple, collector); exception = new ContractViolationReportedFailedException(invalidOutputContracts, this.id); } } if (exception != null) { throw exception; } } protected void handleInputError(ValidatedContract validatedInputContract, String id, Tuple tuple) { // Do nothing } @Override public void cleanup() { delegate.cleanup(); } public TContractsBolt getContractBolt() { return this.delegate; } private ValidatedContract transformAndValidateInput(final Object contract) { try { TInput input = transformInput(contract); return ContractValidator.instance().validate(input); } catch (JsonProcessingException e) { return new ValidatedContract(null, new ValidationException(e)); } } private boolean isOfTypeInput(Object contract) { return reflector.getInputClass().equals(contract.getClass()); } /** * Override to support different input formats. * If cannot convert the contract, returns {@code null}. */ protected TInput transformInput(Object contract) throws JsonProcessingException { if (contract == null) { return null; } if (isOfTypeInput(contract)) { return (TInput) contract; } else { //lock is needed to ensure no threads are iterating over the contract in parallel synchronized (contract) { if (contract instanceof ObjectNode) { return ContractConverter.instance().convertObjectNodeToContract((ObjectNode) contract, inputFactory); } else { return ContractConverter.instance().convertContractToContract(contract, inputFactory); } } } } /** * Creates the {@link com.forter.contracts.cache.CacheDAO} used in the class in case TInput supports caching. */ protected CacheDAO<TOutput> createCacheDAO(Map stormConf, TopologyContext context) { return new DummyCacheDAO<>(); } protected void reportCacheStatus(Boolean status, Tuple tuple) { } /** * Override this method for different merging/enrichment strategies */ protected List<Object> enrichAttributes(List<Object> update, Tuple originalInput) { Map<String, Object> finalAttributes = new HashMap<>(); finalAttributes.putAll((Map<String, Object>)originalInput.getValue(1)); for (Map.Entry<String, Object> updatedAttribute : ((Map<String, Object>)update.get(1)).entrySet()) { if (updatedAttribute.getValue() != null || !finalAttributes.containsKey(updatedAttribute.getKey())) { finalAttributes.put(updatedAttribute.getKey(), updatedAttribute.getValue()); } } for (String field : outputFieldsToOmit) { finalAttributes.put(field, null); } update.set(1, finalAttributes); return update; } protected List<Object> createOutputTuple(Object id, Object contract) { return Lists.newArrayList(id, transformOutput(contract)); } private void emit(Object id, Object contract, Tuple originalInput, BasicOutputCollector collector) { List<Object> tuple = this.createOutputTuple(id, contract); if(this.isEnrichmentBolt) { tuple = this.enrichAttributes(tuple, originalInput); } collector.emit(Utils.DEFAULT_STREAM_ID, tuple); } private Iterable<Object> iterableContracts(TOutput output) { switch (reflector.getExecuteReturnType()) { case NOT_NULL_CONTRACT: return ImmutableList.of((Object) output); case OPTIONAL_CONTRACT: Optional optional = (Optional) output; if (optional.isPresent()) { return ImmutableList.of(optional.get()); } return ImmutableList.of(); case COLLECTION_CONTRACTS: return (Collection) output; default: Preconditions.checkState(false, "Unsupported executionReturnType " + reflector.getExecuteReturnType()); //suppress compiler warning return null; } } /** * Override this method to provide different output formats. */ protected Object transformOutput(Object output) { return output; } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("id", "validatedContract")); } @Override public Map<String, Object> getComponentConfiguration() { return delegate.getComponentConfiguration(); } protected ContractsBoltReflector getReflector() { return reflector; } static class IsInvalidPredicate implements Predicate<ValidatedContract>, Serializable { @Override public boolean apply(ValidatedContract input) { return !input.isValid(); } } static class ValidateContractTransformation implements Function<Object, ValidatedContract>, Serializable { @Override public ValidatedContract apply(Object input) { return ContractValidator.instance().validate(input); } } }