Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.hyperfoil.core.handlers.JsonParser Maven / Gradle / Ivy
package io.hyperfoil.core.handlers;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.Function;
import io.hyperfoil.api.config.BenchmarkDefinitionException;
import io.hyperfoil.api.config.InitFromParam;
import io.hyperfoil.api.config.Locator;
import io.hyperfoil.api.processor.Processor;
import io.hyperfoil.api.processor.Transformer;
import io.hyperfoil.api.session.ResourceUtilizer;
import io.hyperfoil.api.session.Session;
import io.hyperfoil.core.builders.ServiceLoadedBuilderProvider;
import io.hyperfoil.core.data.DataFormat;
import io.hyperfoil.core.generators.Pattern;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
public abstract class JsonParser implements Serializable, ResourceUtilizer {
protected static final Logger log = LoggerFactory.getLogger(JsonParser.class);
protected static final int MAX_PARTS = 16;
protected final String query;
protected final boolean delete;
protected final Transformer replace;
protected final Processor processor;
private final JsonParser.Selector[] selectors;
private StreamQueue.Consumer record = JsonParser.this::record;
public JsonParser(String query, boolean delete, Transformer replace, Processor processor) {
this.query = query;
this.delete = delete;
this.replace = replace;
this.processor = processor;
byte[] queryBytes = query.getBytes(StandardCharsets.UTF_8);
if (queryBytes.length == 0 || queryBytes[0] != '.') {
throw new BenchmarkDefinitionException("Path should start with '.'");
}
ArrayList selectors = new ArrayList<>();
int next = 1;
for (int i = 1; i < queryBytes.length; ++i) {
if (queryBytes[i] == '[' || queryBytes[i] == '.' && next < i) {
while (queryBytes[next] == '.') ++next;
if (next != i) {
selectors.add(new AttribSelector(Arrays.copyOfRange(queryBytes, next, i)));
}
next = i + 1;
}
if (queryBytes[i] == '[') {
ArraySelector arraySelector = new ArraySelector();
++i;
int startIndex = i, endIndex = i;
for (; i < queryBytes.length; ++i) {
if (queryBytes[i] == ']') {
if (endIndex < i) {
arraySelector.rangeEnd = bytesToInt(queryBytes, startIndex, i);
if (startIndex == endIndex) {
arraySelector.rangeStart = arraySelector.rangeEnd;
}
}
selectors.add(arraySelector);
next = i + 1;
break;
} else if (queryBytes[i] == ':') {
if (startIndex < i) {
arraySelector.rangeStart = bytesToInt(queryBytes, startIndex, i);
}
endIndex = i + 1;
}
}
}
}
if (next < queryBytes.length) {
while (queryBytes[next] == '.') ++next;
selectors.add(new AttribSelector(Arrays.copyOfRange(queryBytes, next, queryBytes.length)));
}
this.selectors = selectors.toArray(new JsonParser.Selector[0]);
}
protected abstract void record(Context context, Session session, ByteStream data, int offset, int length, boolean isLastPart);
private static int bytesToInt(byte[] bytes, int start, int end) {
int value = 0;
for (; ; ) {
if (bytes[start] < '0' || bytes[start] > '9') {
throw new BenchmarkDefinitionException("Invalid range specification: " + new String(bytes));
}
value += bytes[start] - '0';
if (++start >= end) {
return value;
} else {
value *= 10;
}
}
}
@Override
public void reserve(Session session) {
ResourceUtilizer.reserve(session, processor, replace);
}
interface Selector extends Serializable {
Context newContext();
interface Context {
void reset();
}
}
private static class AttribSelector implements JsonParser.Selector {
byte[] name;
AttribSelector(byte[] name) {
this.name = name;
}
boolean match(StreamQueue stream, int start, int end) {
assert start <= end;
// TODO: move this to StreamQueue and optimize access
for (int i = 0; i < name.length && i < end - start; ++i) {
if (name[i] != stream.getByte(start + i)) return false;
}
return true;
}
@Override
public Context newContext() {
return null;
}
}
private static class ArraySelector implements Selector {
int rangeStart = 0;
int rangeEnd = Integer.MAX_VALUE;
@Override
public Context newContext() {
return new ArraySelectorContext();
}
boolean matches(ArraySelectorContext context) {
return context.active && context.currentItem >= rangeStart && context.currentItem <= rangeEnd;
}
}
private static class ArraySelectorContext implements Selector.Context {
boolean active;
int currentItem;
@Override
public void reset() {
active = false;
currentItem = 0;
}
}
protected abstract class Context implements Session.Resource {
Selector.Context[] selectorContext = new Selector.Context[selectors.length];
int level;
int selectorLevel;
int selector;
boolean inQuote;
boolean inKey;
boolean escaped;
StreamQueue stream = new StreamQueue(MAX_PARTS);
int keyStartIndex;
int lastCharIndex; // end of key name
int valueStartIndex;
int lastOutputIndex; // last byte we have written out
int safeOutputIndex; // last byte we could definitely write out
ByteStream[] pool = new ByteStream[MAX_PARTS];
protected ByteBuf replaceBuffer = PooledByteBufAllocator.DEFAULT.buffer();
final StreamQueue.Consumer replaceConsumer = this::replaceConsumer;
protected Context(Function byteStreamSupplier) {
for (int i = 0; i < pool.length; ++i) {
pool[i] = byteStreamSupplier.apply(this);
}
for (int i = 0; i < selectors.length; ++i) {
selectorContext[i] = selectors[i].newContext();
}
reset();
}
public void reset() {
for (Selector.Context ctx : selectorContext) {
if (ctx != null) ctx.reset();
}
level = -1;
selectorLevel = 0;
selector = 0;
inQuote = false;
inKey = false;
escaped = false;
keyStartIndex = -1;
lastCharIndex = -1;
valueStartIndex = -1;
lastOutputIndex = 0;
safeOutputIndex = 0;
stream.reset();
replaceBuffer.clear();
}
private Selector.Context current() {
return selectorContext[selector];
}
public void parse(ByteStream data, Session session, boolean isLast) {
int readerIndex = stream.append(data);
PARSING:
while (true) {
int b = stream.getByte(readerIndex++);
switch (b) {
case -1:
--readerIndex;
break PARSING;
case ' ':
case '\n':
case '\t':
case '\r':
// ignore whitespace
break;
case '\\':
escaped = !escaped;
break;
case '{':
if (!inQuote) {
++level;
inKey = true;
if (valueStartIndex < 0) {
safeOutputIndex = readerIndex;
}
// TODO assert we have active attrib selector
}
break;
case '}':
if (!inQuote) {
tryRecord(session, readerIndex);
if (level == selectorLevel) {
--selectorLevel;
--selector;
}
if (valueStartIndex < 0) {
safeOutputIndex = readerIndex;
}
--level;
}
break;
case '"':
if (!escaped) {
inQuote = !inQuote;
}
break;
case ':':
if (!inQuote) {
if (selectorLevel == level && keyStartIndex >= 0 && selector < selectors.length && selectors[selector] instanceof AttribSelector) {
AttribSelector selector = (AttribSelector) selectors[this.selector];
if (selector.match(stream, keyStartIndex, lastCharIndex)) {
if (onMatch(readerIndex) && (delete || replace != null)) {
// omit key's starting quote
int outputEnd = keyStartIndex - 1;
// remove possible comma before the key
LOOP:
while (true) {
switch (stream.getByte(outputEnd - 1)) {
case ' ':
case '\n':
case '\t':
case '\r':
case ',':
--outputEnd;
break;
default:
break LOOP;
}
}
stream.consume(lastOutputIndex, outputEnd, record, this, session, false);
lastOutputIndex = outputEnd;
}
}
}
keyStartIndex = -1;
if (valueStartIndex < 0) {
safeOutputIndex = readerIndex;
}
inKey = false;
}
break;
case ',':
if (!inQuote) {
inKey = true;
keyStartIndex = -1;
tryRecord(session, readerIndex);
if (selectorLevel == level && selector < selectors.length && current() instanceof ArraySelectorContext) {
ArraySelectorContext asc = (ArraySelectorContext) current();
if (asc.active) {
asc.currentItem++;
}
if (((ArraySelector) selectors[selector]).matches(asc)) {
if (onMatch(readerIndex) && (delete || replace != null)) {
// omit the ','
stream.consume(lastOutputIndex, readerIndex - 1, record, this, session, false);
lastOutputIndex = readerIndex - 1;
}
}
}
}
break;
case '[':
if (!inQuote) {
if (valueStartIndex < 0) {
safeOutputIndex = readerIndex;
}
++level;
if (selectorLevel == level && selector < selectors.length && selectors[selector] instanceof ArraySelector) {
ArraySelectorContext asc = (ArraySelectorContext) current();
asc.active = true;
if (((ArraySelector) selectors[selector]).matches(asc)) {
if (onMatch(readerIndex) && (delete || replace != null)) {
stream.consume(lastOutputIndex, readerIndex, record, this, session, false);
lastOutputIndex = readerIndex;
}
}
}
}
break;
case ']':
if (!inQuote) {
tryRecord(session, readerIndex);
if (selectorLevel == level && selector < selectors.length && current() instanceof ArraySelectorContext) {
ArraySelectorContext asc = (ArraySelectorContext) current();
asc.active = false;
--selectorLevel;
}
if (valueStartIndex < 0) {
safeOutputIndex = readerIndex;
}
--level;
}
break;
default:
lastCharIndex = readerIndex;
if (inKey && keyStartIndex < 0) {
keyStartIndex = readerIndex - 1;
}
}
if (b != '\\') {
escaped = false;
}
}
if (keyStartIndex >= 0 || valueStartIndex >= 0) {
stream.release(Math.min(Math.min(keyStartIndex, valueStartIndex), safeOutputIndex));
if (isLast) {
throw new IllegalStateException("End of input while the JSON is not complete.");
}
} else {
if ((delete || replace != null) && lastOutputIndex < safeOutputIndex) {
stream.consume(lastOutputIndex, safeOutputIndex, record, this, session, isLast);
lastOutputIndex = safeOutputIndex;
}
stream.release(readerIndex);
}
}
private boolean onMatch(int readerIndex) {
++selector;
if (selector < selectors.length) {
++selectorLevel;
return false;
} else {
valueStartIndex = readerIndex;
return true;
}
}
private void tryRecord(Session session, int readerIndex) {
if (selectorLevel == level && valueStartIndex >= 0) {
// valueStartIndex is always before quotes here
LOOP:
while (true) {
switch (stream.getByte(valueStartIndex)) {
case ' ':
case '\n':
case '\r':
case '\t':
++valueStartIndex;
break;
case -1:
default:
break LOOP;
}
}
int end = readerIndex - 1;
LOOP:
while (end > valueStartIndex) {
switch (stream.getByte(end - 1)) {
case ' ':
case '\n':
case '\r':
case '\t':
--end;
break;
default:
break LOOP;
}
}
if (valueStartIndex == end) {
// This happens when we try to select from a 0-length array
// - as long as there are not quotes there's nothing to record.
valueStartIndex = -1;
--selector;
return;
}
if (replace != null) {
// The buffer cannot be overwritten as if the processor is caching input
// (this happens when we're defragmenting) we would overwrite the underlying data
replaceBuffer.readerIndex(replaceBuffer.writerIndex());
stream.consume(valueStartIndex, end, replaceConsumer, null, session, true);
// If the result is empty, don't write the key
if (replaceBuffer.isReadable()) {
stream.consume(lastOutputIndex, valueStartIndex, record, this, session, false);
processor.process(session, replaceBuffer, replaceBuffer.readerIndex(), replaceBuffer.readableBytes(), false);
}
} else if (!delete) {
stream.consume(valueStartIndex, end, record, this, session, true);
}
lastOutputIndex = end;
valueStartIndex = -1;
--selector;
}
}
public ByteStream retain(ByteStream stream) {
for (int i = 0; i < pool.length; ++i) {
ByteStream pooled = pool[i];
if (pooled != null) {
pool[i] = null;
stream.moveTo(pooled);
return pooled;
}
}
throw new IllegalStateException();
}
public void release(ByteStream stream) {
for (int i = 0; i < pool.length; ++i) {
if (pool[i] == null) {
pool[i] = stream;
return;
}
}
throw new IllegalStateException();
}
protected abstract void replaceConsumer(Void ignored, Session session, ByteStream data, int offset, int length, boolean lastFragment);
}
public abstract static class BaseBuilder> implements InitFromParam {
protected Locator locator;
protected String query;
protected boolean unquote = true;
protected Processor.Builder> processor;
protected DataFormat format = DataFormat.STRING;
protected boolean delete;
protected Transformer.Builder replace;
/**
* @param param Either query -> variable
or variable <- query
.
* @return Self.
*/
@Override
public S init(String param) {
String query;
String var;
if (param.contains("->")) {
String[] parts = param.split("->");
query = parts[0];
var = parts[1];
} else if (param.contains("<-")) {
String[] parts = param.split("->");
query = parts[1];
var = parts[0];
} else {
throw new BenchmarkDefinitionException("Cannot parse json query specification: '" + param + "', use 'query -> var' or 'var <- query'");
}
return query(query.trim()).toVar(var.trim());
}
@SuppressWarnings("unchecked")
protected S self() {
return (S) this;
}
public S setLocator(Locator locator) {
this.locator = locator;
return self();
}
@SuppressWarnings("unchecked")
public S copy(Locator locator) {
S copy;
try {
copy = (S) getClass().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
return copy.setLocator(locator).query(query).unquote(unquote).processor(processor);
}
/**
* Query selecting the part of JSON.
*
* @param query Query.
* @return Self.
*/
public S query(String query) {
this.query = query;
return self();
}
/**
* Automatically unquote and unescape the input values. By default true.
*
* @param unquote Do unquote and unescape?
* @return Builder.
*/
public S unquote(boolean unquote) {
this.unquote = unquote;
return self();
}
/**
* If this is set to true, the selected key will be deleted from the JSON and the modified JSON will be passed
* to the processor
.
*
* @param delete Should the selected query be deleted?
* @return Self.
*/
public S delete(boolean delete) {
this.delete = delete;
return self();
}
/**
* Custom transformation executed on the value of the selected item.
* Note that the output value must contain quotes (if applicable) and be correctly escaped.
*
* @return Builder.
*/
public ServiceLoadedBuilderProvider replace() {
return new ServiceLoadedBuilderProvider<>(Transformer.Builder.class, locator, this::replace);
}
public S replace(Transformer.Builder replace) {
if (replace == null) {
throw new BenchmarkDefinitionException("Calling replace twice!");
}
this.replace = replace;
return self();
}
/**
* Replace value of selected item with value generated through a pattern.
* Note that the result must contain quotes and be correctly escaped.
*
* @param pattern Pattern format.
* @return Self.
*/
public S replace(String pattern) {
return replace(fragmented -> new Pattern(pattern, false)).unquote(false);
}
/**
* Shortcut to store selected parts in an array in the session. Must follow the pattern variable[maxSize]
*
* @param varAndSize Array name.
* @return Self.
*/
public S toArray(String varAndSize) {
return processor(new ArrayRecorder.Builder().init(varAndSize).format(format));
}
/**
* Shortcut to store first match in given variable. Further matches are ignored.
*
* @param var Variable name.
* @return Self.
*/
public S toVar(String var) {
return processor(new SimpleRecorder.Builder().toVar(var).format(format));
}
public S processor(Processor.Builder> processor) {
if (this.processor != null) {
throw new BenchmarkDefinitionException("Processor already set!");
}
this.processor = processor;
return self();
}
/**
* Conversion to apply on the matching parts with 'toVar' or 'toArray' shortcuts.
*
* @param format Data format.
* @return Self.
*/
public S format(DataFormat format) {
this.format = format;
return self();
}
protected void validate() {
if (query == null) {
throw new BenchmarkDefinitionException("Missing 'query'");
} else if (processor == null) {
throw new BenchmarkDefinitionException("Missing processor - use 'processor', 'toVar' or 'toArray'");
}
}
}
}