Package plan_runner.storm_components

Source Code of plan_runner.storm_components.StormThetaJoin

package plan_runner.storm_components;

import gnu.trove.list.array.TIntArrayList;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;

import org.apache.log4j.Logger;

import plan_runner.components.ComponentProperties;
import plan_runner.conversion.TypeConversion;
import plan_runner.operators.AggregateOperator;
import plan_runner.operators.ChainOperator;
import plan_runner.operators.Operator;
import plan_runner.predicates.ComparisonPredicate;
import plan_runner.predicates.Predicate;
import plan_runner.storage.TupleStorage;
import plan_runner.storm_components.synchronization.TopologyKiller;
import plan_runner.thetajoin.indexes.Index;
import plan_runner.thetajoin.matrix_mapping.EquiMatrixAssignment;
import plan_runner.thetajoin.matrix_mapping.MatrixAssignment;
import plan_runner.utilities.MyUtilities;
import plan_runner.utilities.PeriodicAggBatchSend;
import plan_runner.utilities.SystemParameters;
import plan_runner.utilities.statistics.StatisticsUtilities;
import plan_runner.visitors.PredicateCreateIndexesVisitor;
import plan_runner.visitors.PredicateUpdateIndexesVisitor;
import backtype.storm.Config;
import backtype.storm.topology.InputDeclarer;
import backtype.storm.topology.TopologyBuilder;
import backtype.storm.tuple.Tuple;

public class StormThetaJoin extends StormBoltComponent {
  private static final long serialVersionUID = 1L;
  private static Logger LOG = Logger.getLogger(StormThetaJoin.class);
  private final TupleStorage _firstRelationStorage, _secondRelationStorage;
  private final String _firstEmitterIndex, _secondEmitterIndex;
  private long _numSentTuples = 0;
  private final ChainOperator _operatorChain;
  // position to test for equality in first and second emitter
  // join params of current storage then other relation interchangably !!
  List<Integer> _joinParams;
  private final Predicate _joinPredicate;
  // private OptimalPartition _partitioning;
  private List<Index> _firstRelationIndexes, _secondRelationIndexes;
  private List<Integer> _operatorForIndexes;
  private List<Object> _typeOfValueIndexed;
  private boolean _existIndexes = false;
  // for agg batch sending
  private final Semaphore _semAgg = new Semaphore(1, true);
  private boolean _firstTime = true;
  private PeriodicAggBatchSend _periodicAggBatch;
  private final long _aggBatchOutputMillis;
  private InterchangingComponent _inter = null;
  // for printing statistics for creating graphs
  protected Calendar _cal = Calendar.getInstance();
  protected DateFormat _dateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
  protected SimpleDateFormat _format = new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy");
  protected StatisticsUtilities _statsUtils;

  public StormThetaJoin(StormEmitter firstEmitter, StormEmitter secondEmitter,
      ComponentProperties cp, List<String> allCompNames, Predicate joinPredicate,
      int hierarchyPosition, TopologyBuilder builder, TopologyKiller killer, Config conf,
      InterchangingComponent interComp, boolean isContentSensitive, TypeConversion wrapper) {

    super(cp, allCompNames, hierarchyPosition, conf);

    _firstEmitterIndex = String.valueOf(allCompNames.indexOf(firstEmitter.getName()));
    _secondEmitterIndex = String.valueOf(allCompNames.indexOf(secondEmitter.getName()));
    _aggBatchOutputMillis = cp.getBatchOutputMillis();
    _statsUtils = new StatisticsUtilities(getConf(), LOG);
    final int firstCardinality = SystemParameters
        .getInt(conf, firstEmitter.getName() + "_CARD");
    final int secondCardinality = SystemParameters.getInt(conf, secondEmitter.getName()
        + "_CARD");
    final int parallelism = SystemParameters.getInt(conf, getID() + "_PAR");
    _operatorChain = cp.getChainOperator();
    _joinPredicate = joinPredicate;
    InputDeclarer currentBolt = builder.setBolt(getID(), this, parallelism);
   
    final MatrixAssignment _currentMappingAssignment;
     _currentMappingAssignment = new EquiMatrixAssignment(
        firstCardinality, secondCardinality, parallelism, -1);
   
    final String dim = _currentMappingAssignment.toString();
    LOG.info(getID() + " Initial Dimensions is: " + dim);
   
    if (interComp == null)
      currentBolt = MyUtilities.thetaAttachEmitterComponents(currentBolt, firstEmitter,
          secondEmitter, allCompNames, _currentMappingAssignment, conf,wrapper);
    else {
      currentBolt = MyUtilities.thetaAttachEmitterComponentsWithInterChanging(currentBolt,
          firstEmitter, secondEmitter, allCompNames, _currentMappingAssignment, conf,
          interComp);
      _inter = interComp;
    }
    if (getHierarchyPosition() == FINAL_COMPONENT && (!MyUtilities.isAckEveryTuple(conf)))
      killer.registerComponent(this, parallelism);
    if (cp.getPrintOut() && _operatorChain.isBlocking())
      currentBolt.allGrouping(killer.getID(), SystemParameters.DUMP_RESULTS_STREAM);
    _firstRelationStorage = new TupleStorage();
    _secondRelationStorage = new TupleStorage();
    if (_joinPredicate != null) {
      createIndexes();
      _existIndexes = true;
    } else
      _existIndexes = false;
  }

  @Override
  public void aggBatchSend() {
    if (MyUtilities.isAggBatchOutputMode(_aggBatchOutputMillis))
      if (_operatorChain != null) {
        final Operator lastOperator = _operatorChain.getLastOperator();
        if (lastOperator instanceof AggregateOperator) {
          try {
            _semAgg.acquire();
          } catch (final InterruptedException ex) {
          }
          // sending
          final AggregateOperator agg = (AggregateOperator) lastOperator;
          final List<String> tuples = agg.getContent();
          for (final String tuple : tuples)
            tupleSend(MyUtilities.stringToTuple(tuple, getConf()), null, 0);
          // clearing
          agg.clearStorage();
          _semAgg.release();
        }
      }
  }

  protected void applyOperatorsAndSend(Tuple stormTupleRcv, List<String> tuple,
      long lineageTimestamp, boolean isLastInBatch) {
    if (MyUtilities.isAggBatchOutputMode(_aggBatchOutputMillis))
      try {
        _semAgg.acquire();
      } catch (final InterruptedException ex) {
      }
    tuple = _operatorChain.process(tuple);
    if (MyUtilities.isAggBatchOutputMode(_aggBatchOutputMillis))
      _semAgg.release();
    if (tuple == null)
      return;
    _numSentTuples++;
    printTuple(tuple);
    if (_numSentTuples % _statsUtils.getDipOutputFreqPrint() == 0)
      printStatistics(SystemParameters.OUTPUT_PRINT);
    if (MyUtilities.isSending(getHierarchyPosition(), _aggBatchOutputMillis)) {
      long timestamp = 0;
      if (MyUtilities.isCustomTimestampMode(getConf()))
        if (getHierarchyPosition() == StormComponent.NEXT_TO_LAST_COMPONENT)
          // A tuple has a non-null timestamp only if the component is
          // next to last because we measure the latency of the last operator
          timestamp = System.currentTimeMillis();
      // timestamp = System.nanoTime();
      tupleSend(tuple, stormTupleRcv, timestamp);
    }
    if (MyUtilities.isPrintLatency(getHierarchyPosition(), getConf()))
      printTupleLatency(_numSentTuples - 1, lineageTimestamp);
  }

  private void createIndexes() {
    final PredicateCreateIndexesVisitor visitor = new PredicateCreateIndexesVisitor();
    _joinPredicate.accept(visitor);

    _firstRelationIndexes = new ArrayList<Index>(visitor._firstRelationIndexes);
    _secondRelationIndexes = new ArrayList<Index>(visitor._secondRelationIndexes);
    _operatorForIndexes = new ArrayList<Integer>(visitor._operatorForIndexes);
    _typeOfValueIndexed = new ArrayList<Object>(visitor._typeOfValueIndexed);
  }

  @Override
  public void execute(Tuple stormTupleRcv) {
    if (_firstTime && MyUtilities.isAggBatchOutputMode(_aggBatchOutputMillis)) {
      _periodicAggBatch = new PeriodicAggBatchSend(_aggBatchOutputMillis, this);
      _firstTime = false;
    }

    if (receivedDumpSignal(stormTupleRcv)) {
      MyUtilities.dumpSignal(this, stormTupleRcv, getCollector());
      return;
    }

    if (!MyUtilities.isManualBatchingMode(getConf())) {
      final String inputComponentIndex = stormTupleRcv
          .getStringByField(StormComponent.COMP_INDEX); // getString(0);
      final List<String> tuple = (List<String>) stormTupleRcv
          .getValueByField(StormComponent.TUPLE); // getValue(1);
      final String inputTupleHash = stormTupleRcv.getStringByField(StormComponent.HASH);// getString(2);
      if (processFinalAck(tuple, stormTupleRcv))
        return;
      final String inputTupleString = MyUtilities.tupleToString(tuple, getConf());
      processNonLastTuple(inputComponentIndex, inputTupleString, tuple, inputTupleHash,
          stormTupleRcv, true);
    } else {
      final String inputComponentIndex = stormTupleRcv
          .getStringByField(StormComponent.COMP_INDEX); // getString(0);
      final String inputBatch = stormTupleRcv.getStringByField(StormComponent.TUPLE);// getString(1);
      final String[] wholeTuples = inputBatch
          .split(SystemParameters.MANUAL_BATCH_TUPLE_DELIMITER);
      final int batchSize = wholeTuples.length;
      for (int i = 0; i < batchSize; i++) {
        // parsing
        final String currentTuple = new String(wholeTuples[i]);
        final String[] parts = currentTuple
            .split(SystemParameters.MANUAL_BATCH_HASH_DELIMITER);
        String inputTupleHash = null;
        String inputTupleString = null;
        if (parts.length == 1)
          // lastAck
          inputTupleString = new String(parts[0]);
        else {
          inputTupleHash = new String(parts[0]);
          inputTupleString = new String(parts[1]);
        }
        final List<String> tuple = MyUtilities.stringToTuple(inputTupleString, getConf());
        // final Ack check
        if (processFinalAck(tuple, stormTupleRcv)) {
          if (i != batchSize - 1)
            throw new RuntimeException(
                "Should not be here. LAST_ACK is not the last tuple!");
          return;
        }
        // processing a tuple
        if (i == batchSize - 1)
          processNonLastTuple(inputComponentIndex, inputTupleString, tuple,
              inputTupleHash, stormTupleRcv, true);
        else
          processNonLastTuple(inputComponentIndex, inputTupleString, tuple,
              inputTupleHash, stormTupleRcv, false);
      }
    }
    getCollector().ack(stormTupleRcv);
  }

  @Override
  public ChainOperator getChainOperator() {
    return _operatorChain;
  }

  // from IRichBolt
  @Override
  public Map<String, Object> getComponentConfiguration() {
    return getConf();
  }

  @Override
  public String getInfoID() {
    final String str = "DestinationStorage " + getID() + " has ID: " + getID();
    return str;
  }

  @Override
  protected InterchangingComponent getInterComp() {
    return _inter;
  }

  @Override
  public long getNumSentTuples() {
    return _numSentTuples;
  }

  @Override
  public PeriodicAggBatchSend getPeriodicAggBatch() {
    return _periodicAggBatch;
  }

  private void join(Tuple stormTuple, List<String> tuple, boolean isFromFirstEmitter,
      TupleStorage oppositeStorage, boolean isLastInBatch) {

    if (oppositeStorage == null || oppositeStorage.size() == 0)
      return;
    for (int i = 0; i < oppositeStorage.size(); i++) {
      String oppositeTupleString = oppositeStorage.get(i);
      long lineageTimestamp = 0;
      if (MyUtilities.isCustomTimestampMode(getConf()))
        lineageTimestamp = stormTuple.getLongByField(StormComponent.TIMESTAMP);
      if (MyUtilities.isStoreTimestamp(getConf(), getHierarchyPosition())) {
        // timestamp has to be removed
        final String parts[] = oppositeTupleString.split("\\@");
        final long storedTimestamp = Long.valueOf(new String(parts[0]));
        oppositeTupleString = new String(parts[1]);
        // now we set the maximum TS to the tuple
        if (storedTimestamp > lineageTimestamp)
          lineageTimestamp = storedTimestamp;
      }
      final List<String> oppositeTuple = MyUtilities.stringToTuple(oppositeTupleString,
          getComponentConfiguration());
      List<String> firstTuple, secondTuple;
      if (isFromFirstEmitter) {
        firstTuple = tuple;
        secondTuple = oppositeTuple;
      } else {
        firstTuple = oppositeTuple;
        secondTuple = tuple;
      }
      // Check joinCondition if existIndexes == true, the join condition is already checked before
      if (_joinPredicate == null || _existIndexes
          || _joinPredicate.test(firstTuple, secondTuple)) {
        // if null, cross product
        // Create the output tuple by omitting the oppositeJoinKeys
        // (ONLY for equi-joins since they are added by the first relation),
        //if any (in case of cartesian product there are none)
        List<String> outputTuple = null;
        // Cartesian product - Outputs all attributes
        outputTuple = MyUtilities.createOutputTuple(firstTuple, secondTuple);
        applyOperatorsAndSend(stormTuple, outputTuple, lineageTimestamp, isLastInBatch);
      }
    }
  }

  protected void performJoin(Tuple stormTupleRcv, List<String> tuple, String inputTupleHash,
      boolean isFromFirstEmitter, List<Index> oppositeIndexes,
      List<String> valuesToApplyOnIndex, TupleStorage oppositeStorage, boolean isLastInBatch) {
    final TupleStorage tuplesToJoin = new TupleStorage();
    selectTupleToJoin(oppositeStorage, oppositeIndexes, isFromFirstEmitter,
        valuesToApplyOnIndex, tuplesToJoin);
    join(stormTupleRcv, tuple, isFromFirstEmitter, tuplesToJoin, isLastInBatch);
  }

  @Override
  protected void printStatistics(int type) {
    if (_statsUtils.isTestMode())
      if (getHierarchyPosition() == StormComponent.FINAL_COMPONENT) {
        // computing variables
        final int size1 = _firstRelationStorage.size();
        final int size2 = _secondRelationStorage.size();
        final int totalSize = size1 + size2;
        final String ts = _dateFormat.format(_cal.getTime());

        // printing
        if (!MyUtilities.isCustomTimestampMode(getConf())) {
          final Runtime runtime = Runtime.getRuntime();
          final long memory = runtime.totalMemory() - runtime.freeMemory();
          if (type == SystemParameters.INITIAL_PRINT)
            LOG.info("," + "INITIAL," + _thisTaskID + "," + " TimeStamp:," + ts
                + ", FirstStorage:," + size1 + ", SecondStorage:," + size2
                + ", Total:," + totalSize + ", Memory used: ,"
                + StatisticsUtilities.bytesToMegabytes(memory) + ","
                + StatisticsUtilities.bytesToMegabytes(runtime.totalMemory()));
          else if (type == SystemParameters.INPUT_PRINT)
            LOG.info("," + "MEMORY," + _thisTaskID + "," + " TimeStamp:," + ts
                + ", FirstStorage:," + size1 + ", SecondStorage:," + size2
                + ", Total:," + totalSize + ", Memory used: ,"
                + StatisticsUtilities.bytesToMegabytes(memory) + ","
                + StatisticsUtilities.bytesToMegabytes(runtime.totalMemory()));
          else if (type == SystemParameters.OUTPUT_PRINT)
            LOG.info("," + "RESULT," + _thisTaskID + "," + "TimeStamp:," + ts
                + ",Sent Tuples," + getNumSentTuples());
          else if (type == SystemParameters.FINAL_PRINT) {
            if (numNegatives > 0)
              LOG.info("WARNINGLAT! Negative latency for " + numNegatives
                  + ", at most " + maxNegative + "ms.");
            LOG.info("," + "MEMORY," + _thisTaskID + "," + " TimeStamp:," + ts
                + ", FirstStorage:," + size1 + ", SecondStorage:," + size2
                + ", Total:," + totalSize + ", Memory used: ,"
                + StatisticsUtilities.bytesToMegabytes(memory) + ","
                + StatisticsUtilities.bytesToMegabytes(runtime.totalMemory()));
            LOG.info("," + "RESULT," + _thisTaskID + "," + "TimeStamp:," + ts
                + ",Sent Tuples," + getNumSentTuples());
          }
        } else   // only final statistics is printed if we are measuring
            // latency
        if (type == SystemParameters.FINAL_PRINT) {
          final Runtime runtime = Runtime.getRuntime();
          final long memory = runtime.totalMemory() - runtime.freeMemory();
          if (numNegatives > 0)
            LOG.info("WARNINGLAT! Negative latency for " + numNegatives + ", at most "
                + maxNegative + "ms.");
          LOG.info("," + "MEMORY," + _thisTaskID + "," + " TimeStamp:," + ts
              + ", FirstStorage:," + size1 + ", SecondStorage:," + size2
              + ", Total:," + totalSize + ", Memory used: ,"
              + StatisticsUtilities.bytesToMegabytes(memory) + ","
              + StatisticsUtilities.bytesToMegabytes(runtime.totalMemory()));
          LOG.info("," + "RESULT," + _thisTaskID + "," + "TimeStamp:," + ts
              + ",Sent Tuples," + getNumSentTuples());
        }
      }
  }

  private void processNonLastTuple(String inputComponentIndex, String inputTupleString, //
      List<String> tuple, // these two are the same
      String inputTupleHash, Tuple stormTupleRcv, boolean isLastInBatch) {
    boolean isFromFirstEmitter = false;
    TupleStorage affectedStorage, oppositeStorage;
    List<Index> affectedIndexes, oppositeIndexes;
    if (_firstEmitterIndex.equals(inputComponentIndex)) {
      // R update
      isFromFirstEmitter = true;
      affectedStorage = _firstRelationStorage;
      oppositeStorage = _secondRelationStorage;
      affectedIndexes = _firstRelationIndexes;
      oppositeIndexes = _secondRelationIndexes;
      sendToStatisticsCollector(tuple, 0);
    } else if (_secondEmitterIndex.equals(inputComponentIndex)) {
      // S update
      isFromFirstEmitter = false;
      affectedStorage = _secondRelationStorage;
      oppositeStorage = _firstRelationStorage;
      affectedIndexes = _secondRelationIndexes;
      oppositeIndexes = _firstRelationIndexes;
      sendToStatisticsCollector(tuple, 1);
    } else
      throw new RuntimeException("InputComponentName " + inputComponentIndex
          + " doesn't match neither " + _firstEmitterIndex + " nor "
          + _secondEmitterIndex + ".");
    // add the stormTuple to the specific storage
    if (MyUtilities.isStoreTimestamp(getConf(), getHierarchyPosition())) {
      final long incomingTimestamp = stormTupleRcv.getLongByField(StormComponent.TIMESTAMP);
      inputTupleString = incomingTimestamp + SystemParameters.STORE_TIMESTAMP_DELIMITER
          + inputTupleString;
    }
    final int row_id = affectedStorage.insert(inputTupleString);
    List<String> valuesToApplyOnIndex = null;
    if (_existIndexes)
      valuesToApplyOnIndex = updateIndexes(inputComponentIndex, tuple, affectedIndexes,
          row_id);
    performJoin(stormTupleRcv, tuple, inputTupleHash, isFromFirstEmitter, oppositeIndexes,
        valuesToApplyOnIndex, oppositeStorage, isLastInBatch);
    if ((_firstRelationStorage.size() + _secondRelationStorage.size())
        % _statsUtils.getDipInputFreqPrint() == 0)
      printStatistics(SystemParameters.INPUT_PRINT);
  }

  private void selectTupleToJoin(TupleStorage oppositeStorage, List<Index> oppositeIndexes,
      boolean isFromFirstEmitter, List<String> valuesToApplyOnIndex, TupleStorage tuplesToJoin) {
    if (!_existIndexes) {
      tuplesToJoin.copy(oppositeStorage);
      return;
    }
    final TIntArrayList rowIds = new TIntArrayList();
    // If there is atleast one index (so we have single join conditions with
    // 1 index per condition)
    // Get the row indices in the storage of the opposite relation that
    // satisfy each join condition (equijoin / inequality)
    // Then take the intersection of the returned row indices since each
    // join condition
    // is separated by AND

    for (int i = 0; i < oppositeIndexes.size(); i++) {
      TIntArrayList currentRowIds = null;

      final Index currentOpposIndex = oppositeIndexes.get(i);
      final String value = valuesToApplyOnIndex.get(i);

      int currentOperator = _operatorForIndexes.get(i);
      // Switch inequality operator if the tuple coming is from the other
      // relation
      if (isFromFirstEmitter) {
        final int operator = currentOperator;

        if (operator == ComparisonPredicate.GREATER_OP)
          currentOperator = ComparisonPredicate.LESS_OP;
        else if (operator == ComparisonPredicate.NONGREATER_OP)
          currentOperator = ComparisonPredicate.NONLESS_OP;
        else if (operator == ComparisonPredicate.LESS_OP)
          currentOperator = ComparisonPredicate.GREATER_OP;
        else if (operator == ComparisonPredicate.NONLESS_OP)
          currentOperator = ComparisonPredicate.NONGREATER_OP;
        else
          currentOperator = operator;
      }

      // Get the values from the index (check type first)
      if (_typeOfValueIndexed.get(i) instanceof String)
        currentRowIds = currentOpposIndex.getValues(currentOperator, value);
      // Even if valueIndexed is at first time an integer with
      // precomputation a*col +b, it become a double
      else if (_typeOfValueIndexed.get(i) instanceof Double)
        currentRowIds = currentOpposIndex.getValues(currentOperator,
            Double.parseDouble(value));
      else if (_typeOfValueIndexed.get(i) instanceof Integer)
        currentRowIds = currentOpposIndex.getValues(currentOperator,
            Integer.parseInt(value));
      else if (_typeOfValueIndexed.get(i) instanceof Date)
        try {
          currentRowIds = currentOpposIndex.getValues(currentOperator,
              _format.parse(value));
        } catch (final ParseException e) {
          e.printStackTrace();
        }
      else
        throw new RuntimeException("non supported type");
      // Compute the intersection
      // TODO: Search only within the ids that are in rowIds from previous conditions If
      //nothing returned (and since we want intersection), no need to proceed.
      if (currentRowIds == null)
        return;
      // If it's the first index, add everything. Else keep the
      // intersection
      if (i == 0)
        rowIds.addAll(currentRowIds);
      else
        rowIds.retainAll(currentRowIds);
      // If empty after intersection, return
      if (rowIds.isEmpty())
        return;
    }
    // generate tuplestorage
    for (int i = 0; i < rowIds.size(); i++) {
      final int id = rowIds.get(i);
      tuplesToJoin.insert(oppositeStorage.get(id));
    }
  }

  private List<String> updateIndexes(String inputComponentIndex, List<String> tuple,
      List<Index> affectedIndexes, int row_id) {
    boolean comeFromFirstEmitter;

    if (inputComponentIndex.equals(_firstEmitterIndex))
      comeFromFirstEmitter = true;
    else
      comeFromFirstEmitter = false;
    final PredicateUpdateIndexesVisitor visitor = new PredicateUpdateIndexesVisitor(
        comeFromFirstEmitter, tuple);
    _joinPredicate.accept(visitor);

    final List<String> valuesToIndex = new ArrayList<String>(visitor._valuesToIndex);
    final List<Object> typesOfValuesToIndex = new ArrayList<Object>(
        visitor._typesOfValuesToIndex);

    for (int i = 0; i < affectedIndexes.size(); i++)
      if (typesOfValuesToIndex.get(i) instanceof Integer)
        affectedIndexes.get(i).put(row_id, Integer.parseInt(valuesToIndex.get(i)));
      else if (typesOfValuesToIndex.get(i) instanceof Double)
        affectedIndexes.get(i).put(row_id, Double.parseDouble(valuesToIndex.get(i)));
      else if (typesOfValuesToIndex.get(i) instanceof Date)
        try {
          affectedIndexes.get(i).put(row_id, _format.parse(valuesToIndex.get(i)));
        } catch (final ParseException e) {
          throw new RuntimeException("Parsing problem in StormThetaJoin.updatedIndexes "
              + e.getMessage());
        }
      else if (typesOfValuesToIndex.get(i) instanceof String)
        affectedIndexes.get(i).put(row_id, valuesToIndex.get(i));
      else
        throw new RuntimeException("non supported type");
    return valuesToIndex;
  }
}
TOP

Related Classes of plan_runner.storm_components.StormThetaJoin

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.