blob: c8f482de715294fc30c817a29d8c833c2f3edd84 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.rya.indexing.accumulo.temporal;
import static org.apache.rya.api.resolver.RdfToRyaConversions.convertStatement;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.PrintStream;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.MultiTableBatchWriter;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableExistsException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.admin.TableOperations;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.Text;
import org.apache.rya.api.RdfCloudTripleStoreConfiguration;
import org.apache.rya.api.domain.RyaStatement;
import org.apache.rya.indexing.StatementConstraints;
import org.apache.rya.indexing.StatementSerializer;
import org.apache.rya.indexing.TemporalInstant;
import org.apache.rya.indexing.TemporalInstantRfc3339;
import org.apache.rya.indexing.TemporalInterval;
import org.apache.rya.indexing.accumulo.ConfigUtils;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.collect.Lists;
/**
* JUnit tests for TemporalIndexer and it's implementation AccumuloTemporalIndexer
*
* If you enjoy this test, please read RyaTemporalIndexerTest and YagoKBTest, which contain
* many example SPARQL queries and updates and attempts to test independently of Accumulo:
*
* extras/indexingSail/src/test/java/org.apache/rya/indexing/accumulo/RyaTemporalIndexerTest.java
* {@link org.apache.rya.indexing.accumulo.RyaTemporalIndexerTest}
* {@link org.apache.rya.indexing.accumulo.YagoKBTest.java}
*
* Remember, this class in instantiated fresh for each @test method.
* so fields are reset, unless they are static.
*
* These are covered:
* Instance {before, equals, after} given Instance
* Instance {before, after, inside} given Interval
* Instance {hasBeginning, hasEnd} given Interval
* And a few more.
*
* The temporal predicates are from these ontologies:
* http://linkedevents.org/ontology
* http://motools.sourceforge.net/event/event.html
*/
public final class AccumuloTemporalIndexerTest {
// Configuration properties, this is reset per test in setup.
Configuration conf;
// temporal indexer to test, this is created for each test method by setup.
AccumuloTemporalIndexer tIndexer;
private static final String URI_PROPERTY_EVENT_TIME = "Property:event:time";
private static final String URI_PROPERTY_CIRCA = "Property:circa";
private static final String URI_PROPERTY_AT_TIME = "Property:atTime";
private static final String STAT_COUNT = "count";
private static final String STAT_KEYHASH = "keyhash";
private static final String STAT_VALUEHASH = "valuehash";
private static final StatementConstraints EMPTY_CONSTRAINTS = new StatementConstraints();
// Assign this in setUpBeforeClass, store them in each test.
// setup() deletes table before each test.
static final Statement spo_B00_E01;
static final Statement spo_B03_E20;
static final Statement spo_B02_E29;
static final Statement spo_B02_E30;
static final Statement spo_B02_E40;
static final Statement spo_B02_E31;
static final Statement spo_B29_E30;
static final Statement spo_B30_E32;
// Instants:
static final Statement spo_B02;
static final int SERIES_OF_SECONDS = 41;
static final Statement seriesSpo[] = new Statement[SERIES_OF_SECONDS];
// These are shared for several tests. Only the seconds are different.
// tvB03_E20 read as: interval Begins 3 seconds, ends at 20 seconds
static final TemporalInterval tvB00_E01 = new TemporalInterval(//
makeInstant(00), //
makeInstant(01));
static final TemporalInterval tvB29_E30= new TemporalInterval(//
makeInstant(29), //
makeInstant(30));
static final TemporalInterval tvB30_E32= new TemporalInterval(//
makeInstant(30), //
makeInstant(32));
static final TemporalInterval tvB03_E20 = new TemporalInterval(//
makeInstant(03), //
makeInstant(20));
// 30 seconds, Begins earlier, ends later
static final TemporalInterval tvB02_E30= new TemporalInterval(//
makeInstant(02), //
makeInstant(30));
// use for interval after
static final TemporalInterval tvB02_E29= new TemporalInterval(//
makeInstant(02), //
makeInstant(29));
// same as above, but ends in the middle
static final TemporalInterval tvB02_E31 = new TemporalInterval(//
makeInstant(02), //
makeInstant(31));
// same as above, but ends even later
static final TemporalInterval tvB02_E40 = new TemporalInterval(//
makeInstant(02), //
makeInstant(40));
// instant, match beginnings of several above, before tiB03_E20
static final TemporalInstant tsB02 = makeInstant(02);
// instant, after all above
static final TemporalInstant tsB04 = makeInstant(04);
// Create a series of instants about times 0 - 40 seconds
static final TemporalInstant seriesTs[];
static {
seriesTs = new TemporalInstant[SERIES_OF_SECONDS];
for (int i = 0; i <= 40; i++)
seriesTs[i] = makeInstant(i);
}
/**
* Make an uniform instant with given seconds.
*/
static TemporalInstant makeInstant(int secondsMakeMeUnique) {
return new TemporalInstantRfc3339(2015, 12, 30, 12, 00, secondsMakeMeUnique);
}
static {
// Setup the statements only once. Each test will store some of these in there own index table.
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
// tiB03_E20 read as: time interval that Begins 3 seconds, ends at 20 seconds,
// Each time element the same, except seconds. year, month, .... minute are the same for each statement below.
spo_B00_E01 = vf.createStatement(vf.createIRI("foo:event0"), pred1_atTime, vf.createLiteral(tvB00_E01.toString()));
spo_B02_E29 = vf.createStatement(vf.createIRI("foo:event2"), pred1_atTime, vf.createLiteral(tvB02_E29.toString()));
spo_B02_E30 = vf.createStatement(vf.createIRI("foo:event2"), pred1_atTime, vf.createLiteral(tvB02_E30.toString()));
spo_B02_E31 = vf.createStatement(vf.createIRI("foo:event3"), pred1_atTime, vf.createLiteral(tvB02_E31.toString()));
spo_B02_E40 = vf.createStatement(vf.createIRI("foo:event4"), pred1_atTime, vf.createLiteral(tvB02_E40.toString()));
spo_B03_E20 = vf.createStatement(vf.createIRI("foo:event5"), pred1_atTime, vf.createLiteral(tvB03_E20.toString()));
spo_B29_E30 = vf.createStatement(vf.createIRI("foo:event1"), pred1_atTime, vf.createLiteral(tvB29_E30.toString()));
spo_B30_E32 = vf.createStatement(vf.createIRI("foo:event1"), pred1_atTime, vf.createLiteral(tvB30_E32.toString()));
spo_B02 = vf.createStatement(vf.createIRI("foo:event6"), pred1_atTime, vf.createLiteral(tsB02.getAsReadable()));
// Create statements about time instants 0 - 40 seconds
for (int i = 0; i < seriesTs.length; i++) {
seriesSpo[i] = vf.createStatement(vf.createIRI("foo:event0" + i), pred1_atTime, vf.createLiteral(seriesTs[i].getAsReadable()));
}
}
/**
* @throws java.lang.Exception
*/
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
/**
* @throws java.lang.Exception
*/
@AfterClass
public static void tearDownAfterClass() throws Exception {
}
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
conf = new Configuration();
conf.set(RdfCloudTripleStoreConfiguration.CONF_TBL_PREFIX, "triplestore_");
conf.setBoolean(ConfigUtils.USE_MOCK_INSTANCE, true);
// The temporal predicates are from http://linkedevents.org/ontology
// and http://motools.sourceforge.net/event/event.html
conf.setStrings(ConfigUtils.TEMPORAL_PREDICATES_LIST, ""
+ URI_PROPERTY_AT_TIME + ","
+ URI_PROPERTY_CIRCA + ","
+ URI_PROPERTY_EVENT_TIME);
tIndexer = new AccumuloTemporalIndexer();
tIndexer.setConf(conf);
Connector connector = ConfigUtils.getConnector(conf);
MultiTableBatchWriter mt_bw = connector.createMultiTableBatchWriter(new BatchWriterConfig());
tIndexer.setConnector(connector);
tIndexer.setMultiTableBatchWriter(mt_bw);
tIndexer.init();
}
/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
String indexTableName = tIndexer.getTableName();
tIndexer.close();
TableOperations tableOps = ConfigUtils.getConnector(conf).tableOperations();
if (tableOps.exists(indexTableName))
tableOps.delete(indexTableName);
}
/**
* Test method for {@link AccumuloTemporalIndexer#TemporalIndexerImpl(org.apache.hadoop.conf.Configuration)} .
*
* @throws TableExistsException
* @throws TableNotFoundException
* @throws AccumuloSecurityException
* @throws AccumuloException
* @throws IOException
*/
@Test
public void testTemporalIndexerImpl()
throws AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, IOException {
assertNotNull("Constructed.", tIndexer.toString());
}
/**
* Test method for {@link AccumuloTemporalIndexer#storeStatement(convertStatement(org.eclipse.rdf4j.model.Statement)}
*
* @throws NoSuchAlgorithmException
*/
@Test
public void testStoreStatement() throws IOException, AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, NoSuchAlgorithmException {
// count rows expected to store:
int rowsStoredExpected = 0;
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
IRI pred2_circa = vf.createIRI(URI_PROPERTY_CIRCA);
// Should not be stored because they are not in the predicate list
String validDateStringWithThirteens = "1313-12-13T13:13:13Z";
tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj1"), RDFS.LABEL, vf.createLiteral(validDateStringWithThirteens))));
// Test: Should not store an improper date, and log a warning (log warning not tested).
final String invalidDateString = "ThisIsAnInvalidDate";
// // Silently logs a warning for bad dates. Old: Set true when we catch the error:
// boolean catchErrorThrownCorrectly = false;
// try {
tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj2"), pred1_atTime, vf.createLiteral(invalidDateString))));
// } catch (IllegalArgumentException e) {
// catchErrorThrownCorrectly = true;
// Assert.assertTrue(
// "Invalid date parse error should include the invalid string. message=" + e.getMessage(),
// e.getMessage().contains(invalidDateString));
// }
// Assert.assertTrue("Invalid date parse error should be thrown for this bad date=" + invalidDateString, catchErrorThrownCorrectly);
// These are different datetimes instant but from different time zones.
// This is an arbitrary zone, BRST=Brazil, better if not local.
// same as "2015-01-01T01:59:59Z"
final String testDate2014InBRST = "2014-12-31T23:59:59-02:00";
// next year, same as "2017-01-01T01:59:59Z"
final String testDate2016InET = "2016-12-31T20:59:59-05:00";
// These should be stored because they are in the predicate list.
// BUT they will get converted to the same exact datetime in UTC.
Statement s3 = vf.createStatement(vf.createIRI("foo:subj3"), pred1_atTime, vf.createLiteral(testDate2014InBRST));
Statement s4 = vf.createStatement(vf.createIRI("foo:subj4"), pred2_circa, vf.createLiteral(testDate2016InET));
tIndexer.storeStatement(convertStatement(s3));
rowsStoredExpected++;
tIndexer.storeStatement(convertStatement(s4));
rowsStoredExpected++;
// This should not be stored because the object is not a literal
tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj5"), pred1_atTime, vf.createIRI("in:valid"))));
tIndexer.flush();
int rowsStoredActual = printTables("junit testing: Temporal entities stored in testStoreStatement", null, null);
assertEquals("Number of rows stored.", rowsStoredExpected*4, rowsStoredActual); // 4 index entries per statement
}
@Test
public void testDelete() throws IOException, AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, NoSuchAlgorithmException {
// count rows expected to store:
int rowsStoredExpected = 0;
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
IRI pred2_circa = vf.createIRI(URI_PROPERTY_CIRCA);
final String testDate2014InBRST = "2014-12-31T23:59:59-02:00";
final String testDate2016InET = "2016-12-31T20:59:59-05:00";
// These should be stored because they are in the predicate list.
// BUT they will get converted to the same exact datetime in UTC.
Statement s1 = vf.createStatement(vf.createIRI("foo:subj3"), pred1_atTime, vf.createLiteral(testDate2014InBRST));
Statement s2 = vf.createStatement(vf.createIRI("foo:subj4"), pred2_circa, vf.createLiteral(testDate2016InET));
tIndexer.storeStatement(convertStatement(s1));
rowsStoredExpected++;
tIndexer.storeStatement(convertStatement(s2));
rowsStoredExpected++;
tIndexer.flush();
int rowsStoredActual = printTables("junit testing: Temporal entities stored in testDelete before delete", System.out, null);
Assert.assertEquals("Number of rows stored.", rowsStoredExpected*4, rowsStoredActual); // 4 index entries per statement
tIndexer.deleteStatement(convertStatement(s1));
tIndexer.deleteStatement(convertStatement(s2));
int afterDeleteRowsStoredActual = printTables("junit testing: Temporal entities stored in testDelete after delete", System.out, null);
Assert.assertEquals("Number of rows stored after delete.", 0, afterDeleteRowsStoredActual);
}
@Test
public void testStoreStatementWithInterestingLiterals() throws Exception {
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
tIndexer.storeStatement(convertStatement(vf.createStatement(
vf.createIRI("foo:subj2"),
pred1_atTime,
vf.createLiteral("A number of organizations located, gathered, or classed together. [Derived from Concise Oxford English Dictionary, 11th Edition, 2008]"))));
int rowsStoredActual = printTables("junit testing: Temporal entities stored in testStoreStatement", null, null);
Assert.assertEquals("Number of rows stored.", 0, rowsStoredActual); // 4 index entries per statement
}
/**
* Test method for {@link AccumuloTemporalIndexer#storeStatement(convertStatement(org.eclipse.rdf4j.model.Statement)}
*
* @throws NoSuchAlgorithmException
*/
@Test
public void testStoreStatementBadInterval() throws IOException, AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, NoSuchAlgorithmException {
// count rows expected to store:
int rowsStoredExpected = 0;
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
// Test: Should not store an improper date interval, and log a warning (log warning not tested).
final String invalidDateIntervalString="[bad,interval]";
// Silently logs a warning for bad dates.
tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj1"), pred1_atTime, vf.createLiteral(invalidDateIntervalString))));
final String validDateIntervalString="[2016-12-31T20:59:59-05:00,2016-12-31T21:00:00-05:00]";
tIndexer.storeStatement(convertStatement(vf.createStatement(vf.createIRI("foo:subj2"), pred1_atTime, vf.createLiteral(validDateIntervalString))));
rowsStoredExpected++;
tIndexer.flush();
int rowsStoredActual = printTables("junit testing: Temporal intervals stored in testStoreStatement", null, null);
Assert.assertEquals("Only good intervals should be stored.", rowsStoredExpected*2, rowsStoredActual); // 2 index entries per interval statement
}
@Test
public void testStoreStatementsSameTime() throws IOException, NoSuchAlgorithmException, AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException {
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
IRI pred2_circa = vf.createIRI(URI_PROPERTY_CIRCA);
// These are the same datetime instant but from different time
// zones.
// This is an arbitrary zone, BRST=Brazil, better if not local.
final String ZONETestDateInBRST = "2014-12-31T23:59:59-02:00";
final String ZONETestDateInZulu = "2015-01-01T01:59:59Z";
final String ZONETestDateInET = "2014-12-31T20:59:59-05:00";
// These all should be stored because they are in the predicate list.
// BUT they will get converted to the same exact datetime in UTC.
// So we have to make the key distinct! Good luck indexer!
Statement s1 = vf.createStatement(vf.createIRI("foo:subj1"), pred2_circa, vf.createLiteral(ZONETestDateInET));
Statement s2 = vf.createStatement(vf.createIRI("foo:subj2"), pred1_atTime, vf.createLiteral(ZONETestDateInZulu));
Statement s3 = vf.createStatement(vf.createIRI("foo:subj3"), pred1_atTime, vf.createLiteral(ZONETestDateInBRST));
int rowsStoredExpected = 0;
tIndexer.storeStatement(convertStatement(s1));
rowsStoredExpected++;
tIndexer.storeStatement(convertStatement(s2));
rowsStoredExpected++;
tIndexer.storeStatement(convertStatement(s3));
rowsStoredExpected++;
int rowsStoredActual = printTables("junit testing: Duplicate times stored", null /*System.out*/, null);
Assert.assertEquals("Number of Duplicate times stored, 1 means duplicates not handled correctly.", rowsStoredExpected*4, rowsStoredActual);
}
/**
* Test method for {@link AccumuloTemporalIndexer#storeStatements(java.util.Collection)} .
*
* @throws TableExistsException
* @throws TableNotFoundException
* @throws AccumuloSecurityException
* @throws AccumuloException
* @throws IOException
* @throws IllegalArgumentException
* @throws NoSuchAlgorithmException
*/
@Test
public void testStoreStatements()
throws AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, IllegalArgumentException, IOException,
NoSuchAlgorithmException {
long valueHash = 0;
Collection<Statement> statements = new ArrayList<Statement>(70);
statements.addAll(Arrays.asList(seriesSpo));
int rowsStoredExpected = statements.size()*4; // instants store 4 each
// hash the expected output:
for (Statement statement : statements) {
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(statement)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(statement)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(statement)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(statement)));
}
statements.add(spo_B02_E30);
rowsStoredExpected += 2; // intervals store two dates
statements.add(spo_B30_E32);
rowsStoredExpected += 2; // intervals store two dates
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(spo_B02_E30)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(spo_B02_E30)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(spo_B30_E32)));
valueHash = hasher(valueHash, StringUtils.getBytesUtf8(StatementSerializer.writeStatement(spo_B30_E32)));
// duplicates will overwrite old ones, no change in the output except timestamps
statements.add(spo_B30_E32);
statements.add(spo_B30_E32);
List<RyaStatement> ryaStatements = Lists.newArrayList();
for (Statement s : statements){ ryaStatements.add(convertStatement(s));}
tIndexer.storeStatements(ryaStatements);
Map<String, Long> statistics = new HashMap<String, Long>();
int rowsStoredActual = printTables("junit testing: StoreStatements multiple statements", null, statistics);
Assert.assertEquals("Number of rows stored.", rowsStoredExpected, rowsStoredActual); // 4 index entries per statement
Assert.assertEquals("value hash.", valueHash, statistics.get(STAT_VALUEHASH).longValue());
}
/**
* test this classe's hash method to check un-ordered results.
*/
@Test
public void testSelfTestHashMethod() {
// self test on the hash method:
long hash01dup1 = hasher(0, new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
long hash01dup2 = hasher(0, new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 });
Assert.assertEquals("same numbers, different sequence, hash should be the same.", hash01dup1, hash01dup2);
// this one fails for sum hash, passes for XOR
long hash02dup1 = hasher(0, new byte[] { 123, 2, 1, 1 });
hash02dup1 = hasher(hash02dup1, new byte[] { 123, 1, 1, 2 });
long hash02dup2 = hasher(0, new byte[] { 123, 1, 1, 2 });
hash02dup2 = hasher(hash02dup2, new byte[] { 123, 1, 3, 0, 0 });
Assert.assertTrue("Different numbers, should be different hashes: " + hash02dup1 + " != " + hash02dup2, hash02dup1 != hash02dup2);
}
/**
* Test instant equal to a given instant.
* From the series: instant {equal, before, after} instant
* @throws AccumuloSecurityException
* @throws AccumuloException
* @throws TableNotFoundException
*/
@Test
public void testQueryInstantEqualsInstant() throws IOException, QueryEvaluationException, TableNotFoundException, AccumuloException, AccumuloSecurityException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
int expectedStoreCount = 5 * 2; // two entries for intervals
// seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
int searchForSeconds = 5;
int expectedResultCount = 1;
for (int s = 0; s <= searchForSeconds + 3; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
expectedStoreCount+=4; //4 entries per statement.
}
tIndexer.flush();
int rowsStoredActual = printTables("junit testing: testQueryInstantEqualsInstant 0 to 8 seconds and 5 intervals stored. expectedStoreCount="+expectedStoreCount, null /*System.out*/, null);
Assert.assertEquals("Should find count of rows.", expectedStoreCount, rowsStoredActual);
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantEqualsInstant(seriesTs[searchForSeconds], EMPTY_CONSTRAINTS); // <== logic here
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[searchForSeconds]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant after a given instant.
* From the series: instant {equal, before, after} instant
* @throws AccumuloSecurityException
* @throws AccumuloException
* @throws TableNotFoundException
*/
@Test
public void testQueryInstantAfterInstant() throws IOException, QueryEvaluationException, TableNotFoundException, AccumuloException, AccumuloSecurityException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
int searchForSeconds = 4;
int expectedResultCount = 9;
for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantAfterInstant(seriesTs[searchForSeconds], EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[searchForSeconds + count + 1]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant before a given instant.
* From the series: instant {equal, before, after} instant
*/
@Test
public void testQueryInstantBeforeInstant() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
int searchForSeconds = 4;
int expectedResultCount = 4;
for (int s = 0; s <= searchForSeconds + 15; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantBeforeInstant(seriesTs[searchForSeconds], EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[count]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant before given interval.
* From the series: Instance {before, after, inside} given Interval
*/
@Test
public void testQueryInstantBeforeInterval() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
TemporalInterval searchForSeconds = tvB02_E31;
int expectedResultCount = 2; // 00 and 01 seconds.
for (int s = 0; s <= 40; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantBeforeInterval(searchForSeconds, EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[count]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant after given interval.
* Instance {before, after, inside} given Interval
*/
@Test
public void testQueryInstantAfterInterval() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
TemporalInterval searchAfterInterval = tvB02_E31; // from 2 to 31 seconds
int endingSeconds = 31;
int expectedResultCount = 9; // 32,33,...,40 seconds.
for (int s = 0; s <= endingSeconds + expectedResultCount; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantAfterInterval(searchAfterInterval, EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[count + endingSeconds + 1]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant inside given interval.
* Instance {before, after, inside} given Interval
*/
@Test
public void testQueryInstantInsideInterval() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
int beginningSeconds = 2; // <== logic here, and next few lines.
int endingSeconds = 31;
int expectedResultCount = endingSeconds - beginningSeconds - 1; // 3,4,...,30 seconds.
for (int s = 0; s <= 40; s++) {
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantInsideInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[count + beginningSeconds + 1]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant is the Beginning of the given interval.
* from the series: Instance {hasBeginning, hasEnd} Interval
*/
@Test
public void testQueryInstantHasBeginningInterval() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
int searchSeconds = 2; // <== logic here, and next few lines.
int expectedResultCount = 1; // 2 seconds.
for (int s = 0; s <= 10; s++) {
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantHasBeginningInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[searchSeconds]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test instant is the end of the given interval.
* from the series: Instance {hasBeginning, hasEnd} Interval
*/
@Test
public void testQueryInstantHasEndInterval() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instants for s seconds after the uniform time.
TemporalInterval searchInsideInterval = tvB02_E31; // from 2 to 31 seconds
int searchSeconds = 31; // <== logic here, and next few lines.
int expectedResultCount = 1; // 31 seconds.
for (int s = 0; s <= 40; s++) {
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryInstantHasEndInterval(searchInsideInterval, EMPTY_CONSTRAINTS);
int count = 0;
while (iter.hasNext()) {
Statement s = iter.next();
Statement nextExpectedStatement = seriesSpo[searchSeconds]; // <== logic here
assertTrue("Should match: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count++;
}
Assert.assertEquals("Should find count of rows.", expectedResultCount, count);
}
/**
* Test method for
* {@link org.apache.rya.indexing.accumulo.temporal.AccumuloTemporalIndexer#queryIntervalEquals(TemporalInterval, StatementConstraints)}
* .
* @throws IOException
* @throws QueryEvaluationException
*
*/
@Test
public void testQueryIntervalEquals() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
tIndexer.storeStatement(convertStatement(seriesSpo[4])); // instance at 4 seconds
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryIntervalEquals(tvB02_E40, EMPTY_CONSTRAINTS);
// Should be found twice:
Assert.assertTrue("queryIntervalEquals: spo_B02_E40 should be found, but actually returned empty results. spo_B02_E40=" + spo_B02_E40, iter.hasNext());
Assert.assertTrue("queryIntervalEquals: spo_B02_E40 should be found, but does not match.", spo_B02_E40.equals(iter.next()));
Assert.assertFalse("queryIntervalEquals: Find no more than one, but actually has more.", iter.hasNext());
}
/**
* Test interval before a given interval, for method:
* {@link AccumuloTemporalIndexer#queryIntervalBefore(TemporalInterval, StatementConstraints)}.
*
* @throws IOException
* @throws QueryEvaluationException
*/
@Test
public void testQueryIntervalBefore() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
tIndexer.storeStatement(convertStatement(spo_B00_E01));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B03_E20));
// instants should be ignored.
tIndexer.storeStatement(convertStatement(spo_B30_E32));
tIndexer.storeStatement(convertStatement(seriesSpo[1])); // instance at 1 seconds
tIndexer.storeStatement(convertStatement(seriesSpo[2]));
tIndexer.storeStatement(convertStatement(seriesSpo[31]));
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryIntervalBefore(tvB02_E31, EMPTY_CONSTRAINTS);
// Should be found twice:
Assert.assertTrue("spo_B00_E01 should be found, but actually returned empty results. spo_B00_E01=" + spo_B00_E01, iter.hasNext());
Assert.assertTrue("spo_B00_E01 should be found, but found another.", spo_B00_E01.equals(iter.next()));
Assert.assertFalse("Find no more than one, but actually has more.", iter.hasNext());
}
/**
* interval is after the given interval. Find interval beginnings after the endings of the given interval.
* {@link AccumuloTemporalIndexer#queryIntervalAfter(TemporalInterval, StatementContraints).
*
* @throws IOException
* @throws QueryEvaluationException
*/
@Test
public void testQueryIntervalAfter() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
tIndexer.storeStatement(convertStatement(spo_B00_E01));
tIndexer.storeStatement(convertStatement(spo_B02_E29)); //<- after this one.
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B29_E30));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// instants should be ignored.
tIndexer.storeStatement(convertStatement(spo_B02));
tIndexer.storeStatement(convertStatement(seriesSpo[1])); // instance at 1 seconds
tIndexer.storeStatement(convertStatement(seriesSpo[2]));
tIndexer.storeStatement(convertStatement(seriesSpo[31]));
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
iter = tIndexer.queryIntervalAfter(tvB02_E29, EMPTY_CONSTRAINTS);
// Should be found twice:
Assert.assertTrue("spo_B30_E32 should be found, but actually returned empty results. spo_B30_E32=" + spo_B30_E32, iter.hasNext());
Statement s = iter.next();
Assert.assertTrue("spo_B30_E32 should be found, but found another. spo_B30_E32="+spo_B30_E32+", but found="+s, spo_B30_E32.equals(s));
Assert.assertFalse("Find no more than one, but actually has more.", iter.hasNext());
}
/**
* Test instant after a given instant WITH two different predicates as constraints.
*/
@Test
public void testQueryWithMultiplePredicates() throws IOException, QueryEvaluationException {
// tiB02_E30 read as: Begins 2 seconds, ends at 30 seconds
// these should not match as they are not instances.
tIndexer.storeStatement(convertStatement(spo_B03_E20));
tIndexer.storeStatement(convertStatement(spo_B02_E30));
tIndexer.storeStatement(convertStatement(spo_B02_E40));
tIndexer.storeStatement(convertStatement(spo_B02_E31));
tIndexer.storeStatement(convertStatement(spo_B30_E32));
// seriesSpo[s] and seriesTs[s] are statements and instant for s seconds after the uniform time.
int searchForSeconds = 4;
int expectedResultCount = 9;
for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
tIndexer.storeStatement(convertStatement(seriesSpo[s]));
}
ValueFactory vf = SimpleValueFactory.getInstance();
IRI pred3_CIRCA_ = vf.createIRI(URI_PROPERTY_CIRCA); // this one to ignore.
IRI pred2_eventTime = vf.createIRI(URI_PROPERTY_EVENT_TIME);
IRI pred1_atTime = vf.createIRI(URI_PROPERTY_AT_TIME);
// add the predicate = EventTime ; Store in an array for verification.
Statement[] SeriesTs_EventTime = new Statement[expectedResultCount+1];
for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
Statement statement = vf.createStatement(vf.createIRI("foo:EventTimeSubj0" + s), pred2_eventTime, vf.createLiteral(seriesTs[s].getAsReadable()));
tIndexer.storeStatement(convertStatement(statement));
if (s>searchForSeconds)
SeriesTs_EventTime[s - searchForSeconds -1 ] = statement;
}
// add the predicate = CIRCA ; to be ignored because it is not in the constraints.
for (int s = 0; s <= searchForSeconds + expectedResultCount; s++) { // <== logic here
Statement statement = vf.createStatement(vf.createIRI("foo:CircaEventSubj0" + s), pred3_CIRCA_, vf.createLiteral(seriesTs[s].getAsReadable()));
tIndexer.storeStatement(convertStatement(statement));
}
tIndexer.flush();
CloseableIteration<Statement, QueryEvaluationException> iter;
StatementConstraints constraints = new StatementConstraints();
constraints.setPredicates(new HashSet<IRI>(Arrays.asList( pred2_eventTime, pred1_atTime )));
iter = tIndexer.queryInstantAfterInstant(seriesTs[searchForSeconds], constraints); // EMPTY_CONSTRAINTS);//
int count_AtTime = 0;
int count_EventTime = 0;
while (iter.hasNext()) {
Statement s = iter.next();
//System.out.println("testQueryWithMultiplePredicates result="+s);
Statement nextExpectedStatement = seriesSpo[searchForSeconds + count_AtTime + 1]; // <== logic here
if (s.getPredicate().equals(pred1_atTime)) {
assertTrue("Should match atTime: " + nextExpectedStatement + " == " + s, nextExpectedStatement.equals(s));
count_AtTime++;
}
else if (s.getPredicate().equals(pred2_eventTime)) {
assertTrue("Should match eventTime: " + SeriesTs_EventTime[count_EventTime] + " == " + s, SeriesTs_EventTime[count_EventTime].equals(s));
count_EventTime++;
} else {
assertTrue("This predicate should not be returned: "+s, false);
}
}
Assert.assertEquals("Should find count of atTime rows.", expectedResultCount, count_AtTime);
Assert.assertEquals("Should find count of eventTime rows.", expectedResultCount, count_EventTime);
}
/**
* Test method for {@link AccumuloTemporalIndexer#getIndexablePredicates()} .
*
* @throws TableExistsException
* @throws TableNotFoundException
* @throws AccumuloSecurityException
* @throws AccumuloException
* @throws IOException
*/
@Test
public void testGetIndexablePredicates() throws AccumuloException, AccumuloSecurityException, TableNotFoundException, TableExistsException, IOException {
Set<IRI> p = tIndexer.getIndexablePredicates();
Assert.assertEquals("number of predicates returned:", 3, p.size());
}
/**
* Count all the entries in the temporal index table, return the count.
* Uses printTables for reliability.
*
*/
public int countAllRowsInTable() throws AccumuloException, AccumuloSecurityException, TableNotFoundException, NoSuchAlgorithmException {
return printTables("Counting rows.", null, null);
}
/**
* Print and gather statistics on the entire index table.
*
* @param description
* Printed to the console to find the test case.
* @param out
* null or System.out or other output to send a listing.
* @param statistics
* Hashes, sums, and counts for assertions.
* @return Count of entries in the index table.
*/
public int printTables(String description, PrintStream out, Map<String, Long> statistics)
throws TableNotFoundException, AccumuloException, AccumuloSecurityException
{
if (out == null) {
out = new PrintStream(new NullOutputStream());
}
out.println("-- start printTables() -- " + description);
String FORMAT = "%-20s %-20s %-40s %-40s\n";
int rowsPrinted = 0;
long keyHasher = 0;
long valueHasher = 0;
final String indexTableName = tIndexer.getTableName();
out.println("Reading : " + indexTableName);
out.format(FORMAT, "--Row--", "--ColumnFamily--", "--ColumnQualifier--", "--Value--");
Scanner s = ConfigUtils.getConnector(conf).createScanner(indexTableName, Authorizations.EMPTY);
for (Entry<Key, org.apache.accumulo.core.data.Value> entry : s) {
rowsPrinted++;
Key k = entry.getKey();
out.format(FORMAT, toHumanString(k.getRow()),
toHumanString(k.getColumnFamily()),
toHumanString(k.getColumnQualifier()),
toHumanString(entry.getValue()));
keyHasher = hasher(keyHasher, (StringUtils.getBytesUtf8(entry.getKey().toStringNoTime())));
valueHasher = hasher(valueHasher, (entry.getValue().get()));
}
out.println();
if (statistics != null) {
statistics.put(STAT_COUNT, (long) rowsPrinted);
statistics.put(STAT_KEYHASH, keyHasher);
statistics.put(STAT_VALUEHASH, valueHasher);
}
return rowsPrinted;
}
/**
* Order independent hashcode.
* Read more: http://stackoverflow.com/questions/18021643/hashing-a-set-of-integers-in-an-order-independent-way
*
* @param hashcode
* @param list
* @return
*/
private static long hasher(long hashcode, byte[] list) {
long sum = 0;
for (byte val : list) {
sum += 1L + val;
}
hashcode ^= sum;
return hashcode;
}
/**
* convert a non-utf8 byte[] and text and value to string and show unprintable bytes as {xx} where x is hex.
* @param value
* @return Human readable representation.
*/
static String toHumanString(Value value) {
return toHumanString(value==null?null:value.get());
}
static String toHumanString(Text text) {
return toHumanString(text==null?null:text.copyBytes());
}
static String toHumanString(byte[] bytes) {
if (bytes==null)
return "{null}";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
if ((b > 0x7e) || (b < 32)) {
sb.append("{");
sb.append(Integer.toHexString( b & 0xff )); // Lop off the sign extended ones.
sb.append("}");
} else if (b == '{'||b == '}') { // Escape the literal braces.
sb.append("{");
sb.append((char)b);
sb.append("}");
} else
sb.append((char)b);
}
return sb.toString();
}
}