Browse Source

Code clean up and JacksonProvider improvements.

pull/41/head
Kalle Stenflo 10 years ago
parent
commit
0d8192170a
  1. 58
      json-path-web-test/src/main/java/com/jayway/jsonpath/web/bench/Bench.java
  2. 2
      json-path-web-test/src/main/java/com/jayway/jsonpath/web/resource/IndexResource.java
  3. 10
      json-path/src/main/java/com/jayway/jsonpath/Configuration.java
  4. 11
      json-path/src/main/java/com/jayway/jsonpath/Option.java
  5. 148
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java
  6. 10
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java
  7. 16
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java
  8. 56
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java
  9. 23
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java
  10. 3
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java
  11. 4
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java
  12. 21
      json-path/src/main/java/com/jayway/jsonpath/internal/spi/json/JacksonProvider.java
  13. 3
      json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java
  14. 6
      json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProviderFactory.java
  15. 4
      json-path/src/test/java/com/jayway/jsonpath/Runner.java

58
json-path-web-test/src/main/java/com/jayway/jsonpath/web/bench/Bench.java

@ -21,12 +21,16 @@ public class Bench {
protected final String json;
protected final String path;
private final boolean value;
private final boolean optionAsValues;
private final boolean optionAllwaysReturnList;
private final boolean optionMergeMultiProps;
public Bench(String json, String path, boolean value) {
public Bench(String json, String path, boolean optionAsValues, boolean optionAllwaysReturnList, boolean optionMergeMultiProps) {
this.json = json;
this.path = path;
this.value = value;
this.optionAsValues = optionAsValues;
this.optionAllwaysReturnList = optionAllwaysReturnList;
this.optionMergeMultiProps = optionMergeMultiProps;
}
public Result runJayway() {
@ -35,33 +39,31 @@ public class Bench {
long time;
Object res = null;
//Configuration configuration = Configuration.defaultConfiguration();
Configuration configuration = Configuration.defaultConfiguration().options(Option.ALWAYS_RETURN_LIST);
if(!value){
configuration = configuration.options(Option.AS_PATH_LIST);
Configuration configuration = Configuration.defaultConfiguration();
if(optionAllwaysReturnList){
configuration = configuration.addOptions(Option.ALWAYS_RETURN_LIST);
}
if(optionMergeMultiProps){
configuration = configuration.addOptions(Option.MERGE_MULTI_PROPS);
}
if (!optionAsValues) {
configuration = configuration.addOptions(Option.AS_PATH_LIST);
}
long now = System.currentTimeMillis();
try {
res = JsonPath.using(configuration).parse(json).read(path);
/*
if(value) {
res = JsonPath.parse(json).read(path);
} else {
res = JsonPath.parse(json).readPathList(path);
}*/
} catch (Exception e){
} catch (Exception e) {
error = getError(e);
} finally {
time = System.currentTimeMillis() - now;
if(res instanceof String) {
if (res instanceof String) {
result = "\"" + res + "\"";
} else if(res instanceof Number) {
} else if (res instanceof Number) {
result = res.toString();
} else if(res instanceof Boolean){
} else if (res instanceof Boolean) {
result = res.toString();
} else {
result = res != null ? JsonProviderFactory.createProvider().toJson(res) : "null";
@ -78,7 +80,7 @@ public class Bench {
Iterator<Object> query = null;
long now = System.currentTimeMillis();
try {
if(!value){
if (!optionAsValues) {
throw new UnsupportedOperationException("Not supported!");
}
io.gatling.jsonpath.JsonPath jsonPath = JsonPath$.MODULE$.compile(path).right().get();
@ -87,12 +89,12 @@ public class Bench {
Object jsonModel = jsonParser.parse(json);
query = jsonPath.query(jsonModel);
} catch (Exception e){
} catch (Exception e) {
error = getError(e);
} finally {
time = System.currentTimeMillis() - now;
if(query != null) {
if (query != null) {
List<Object> res = new ArrayList<Object>();
while (query.hasNext()) {
res.add(query.next());
@ -113,27 +115,27 @@ public class Bench {
long now = System.currentTimeMillis();
try {
if(!value){
if (!optionAsValues) {
throw new UnsupportedOperationException("Not supported!");
}
com.nebhale.jsonpath.JsonPath compiled = com.nebhale.jsonpath.JsonPath.compile(path);
res = compiled.read(json, Object.class);
} catch (Exception e){
} catch (Exception e) {
error = getError(e);
} finally {
time = System.currentTimeMillis() - now;
result = res!=null? jacksonProvider.toJson(res):null;
result = res != null ? jacksonProvider.toJson(res) : null;
return new Result("nebhale", time, result, error);
}
}
public List<Result> runAll(){
public List<Result> runAll() {
return asList(runJayway(), runBoon(), runNebhale());
}
private String getError(Exception e){
private String getError(Exception e) {
String ex = e.getMessage();
if(ex == null || ex.trim().isEmpty()){
if (ex == null || ex.trim().isEmpty()) {
ex = "Undefined error";
}
return ex;

2
json-path-web-test/src/main/java/com/jayway/jsonpath/web/resource/IndexResource.java

@ -45,7 +45,7 @@ public class IndexResource {
boolean value = "VALUE".equalsIgnoreCase(type);
return createView(json, path, value, template, new Bench(json, path, value).runAll());
return createView(json, path, value, template, new Bench(json, path, value, true, true).runAll());
}
private Viewable createView(String json, String path, boolean value, String selectedTemplate, List<Result> results){

10
json-path/src/main/java/com/jayway/jsonpath/Configuration.java

@ -17,12 +17,12 @@ package com.jayway.jsonpath;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.json.JsonProviderFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import static com.jayway.jsonpath.internal.Utils.notNull;
import static java.util.Arrays.asList;
public class Configuration {
@ -44,6 +44,12 @@ public class Configuration {
return provider;
}
public Configuration addOptions(Option... options) {
EnumSet<Option> opts = EnumSet.noneOf(Option.class);
opts.addAll(this.options);
opts.addAll(asList(options));
return Configuration.builder().jsonProvider(provider).options(opts).build();
}
public Configuration options(Option... options) {
return Configuration.builder().jsonProvider(provider).options(options).build();
}
@ -71,7 +77,7 @@ public class Configuration {
}
public ConfigurationBuilder options(Option... flags) {
this.options.addAll(Arrays.asList(flags));
this.options.addAll(asList(flags));
return this;
}

11
json-path/src/main/java/com/jayway/jsonpath/Option.java

@ -23,15 +23,18 @@ public enum Option {
/**
* Makes this implementation more compliant to the Goessner spec. All results are returned as Lists.
*
*/
ALWAYS_RETURN_LIST,
/**
* returns a list of path strings representing the path of the evaluation hits
*
* Returns a list of path strings representing the path of the evaluation hits
*/
AS_PATH_LIST
AS_PATH_LIST,
/**
* When multiple properties are queried eg @..['foo', 'bar'] these properties are extracted and put in a new Map.
*/
MERGE_MULTI_PROPS
}

148
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java

@ -46,75 +46,87 @@ class ArrayPathToken extends PathToken {
}
try {
if (operation == Operation.SINGLE_INDEX) {
handleArrayIndex(criteria.get(0), currentPath, model, ctx);
} else if (operation == Operation.INDEX_SEQUENCE) {
for (Integer idx : criteria) {
handleArrayIndex(criteria.get(idx), currentPath, model, ctx);
int idx;
int input;
int length;
int from;
int to;
switch (operation){
case SINGLE_INDEX:
handleArrayIndex(criteria.get(0), currentPath, model, ctx);
break;
case INDEX_SEQUENCE:
for (Integer i : criteria) {
handleArrayIndex(criteria.get(i), currentPath, model, ctx);
}
break;
case CONTEXT_SIZE:
length = ctx.jsonProvider().length(model);
idx = length + criteria.get(0);
handleArrayIndex(idx, currentPath, model, ctx);
break;
case SLICE_FROM: //[2:]
input = criteria.get(0);
length = ctx.jsonProvider().length(model);
from = input;
if (from < 0) {
//calculate slice start from array length
from = length + from;
}
from = Math.max(0, from);
logger.debug("Slice from index on array with length: {}. From index: {} to: {}. Input: {}", length, from, length - 1, toString());
if (length == 0 || from >= length) {
return;
}
for (int i = from; i < length; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
break;
case SLICE_TO : //[:2]
input = criteria.get(0);
length = ctx.jsonProvider().length(model);
to = input;
if (to < 0) {
//calculate slice end from array length
to = length + to;
}
to = Math.min(length, to);
logger.debug("Slice to index on array with length: {}. From index: 0 to: {}. Input: {}", length, to, toString());
if (length == 0) {
return;
}
for (int i = 0; i < to; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
break;
case SLICE_BETWEEN : //[2:4]
from = criteria.get(0);
to = criteria.get(1);
length = ctx.jsonProvider().length(model);
to = Math.min(length, to);
if (from >= to || length == 0) {
return;
}
logger.debug("Slice between indexes on array with length: {}. From index: {} to: {}. Input: {}", length, from, to, toString());
for (int i = from; i < to; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
break;
}
} else if (Operation.CONTEXT_SIZE == operation) {
int length = ctx.jsonProvider().length(model);
int idx = length + criteria.get(0);
handleArrayIndex(idx, currentPath, model, ctx);
}
//[2:]
else if (Operation.SLICE_FROM == operation) {
int input = criteria.get(0);
int length = ctx.jsonProvider().length(model);
int from = input;
if (from < 0) {
//calculate slice start from array length
from = length + from;
}
from = Math.max(0, from);
logger.debug("Slice from index on array with length: {}. From index: {} to: {}. Input: {}", length, from, length - 1, toString());
if (length == 0 || from >= length) {
return;
}
for (int i = from; i < length; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
}
//[:2]
else if (Operation.SLICE_TO == operation) {
int input = criteria.get(0);
int length = ctx.jsonProvider().length(model);
int to = input;
if (to < 0) {
//calculate slice end from array length
to = length + to;
}
to = Math.min(length, to);
logger.debug("Slice to index on array with length: {}. From index: 0 to: {}. Input: {}", length, to, toString());
if (length == 0) {
return;
}
for (int i = 0; i < to; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
}
//[2:4]
else if (Operation.SLICE_BETWEEN == operation) {
int from = criteria.get(0);
int to = criteria.get(1);
int length = ctx.jsonProvider().length(model);
to = Math.min(length, to);
if (from >= to || length == 0) {
return;
}
logger.debug("Slice between indexes on array with length: {}. From index: {} to: {}. Input: {}", length, from, to, toString());
for (int i = from; i < to; i++) {
handleArrayIndex(i, currentPath, model, ctx);
}
}
} catch (IndexOutOfBoundsException e) {
throw new PathNotFoundException("Index out of bounds when evaluating path " + currentPath);
}

10
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java

@ -1,7 +1,6 @@
package com.jayway.jsonpath.internal.spi.compiler;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.internal.JsonFormatter;
import com.jayway.jsonpath.spi.compiler.EvaluationContext;
import com.jayway.jsonpath.spi.compiler.Path;
import org.slf4j.Logger;
@ -27,10 +26,6 @@ class CompiledPath implements Path {
EvaluationContextImpl ctx = new EvaluationContextImpl(this, configuration);
root.evaluate("", model, ctx);
if(logger.isDebugEnabled()) {
logger.debug("Found:\n{}", JsonFormatter.prettyPrint(ctx.configuration().getProvider().toJson(ctx.getPathList())));
}
return ctx;
}
@ -39,11 +34,6 @@ class CompiledPath implements Path {
return root.isPathDefinite();
}
@Override
public int tokenCount() {
return root.getTokenCount();
}
@Override
public String toString() {
return root.toString();

16
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java

@ -45,7 +45,7 @@ public class PathCompiler {
String cacheKey = path + filterList.toString();
Path p = cache.get(cacheKey);
if(p != null){
return p;
//return p;
}
RootPathToken root = null;
@ -340,37 +340,31 @@ public class PathCompiler {
//"['foo']"
private PathToken analyzeProperty() {
String property = null;
List<String> properties = new ArrayList<String>();
StringBuilder buffer = new StringBuilder();
boolean propertyIsOpen = false;
boolean propertyDone = false;
while (current != ']') {
switch (current) {
case '\'':
if (propertyIsOpen) {
property = buffer.toString();
properties.add(buffer.toString());
buffer = new StringBuilder();
propertyIsOpen = false;
propertyDone = true;
} else {
propertyIsOpen = true;
}
break;
default:
if (propertyIsOpen) {
if(propertyDone){
//Don't understand how to create a "Normalized path expressions" for this type of query
throw new InvalidPathException("Only single properties are supported.");
}
buffer.append(current);
}
break;
}
current = chars[++i];
}
return new PropertyPathToken(property);
return new PropertyPathToken(properties);
}

56
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java

@ -1,7 +1,11 @@
package com.jayway.jsonpath.internal.spi.compiler;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.internal.Utils;
import java.util.List;
/**
*
@ -16,20 +20,58 @@ abstract class PathToken {
return next;
}
void handleObjectProperty(String currentPath, Object model, EvaluationContextImpl ctx, String property) {
String evalPath = currentPath + "['" + property + "']";
Object propertyVal = readObjectProperty(property, model, ctx);
if (isLeaf()) {
ctx.addResult(evalPath, propertyVal);
void handleObjectProperty(String currentPath, Object model, EvaluationContextImpl ctx, List<String> properties) {
if(properties.size() == 1) {
String property = properties.get(0);
String evalPath = currentPath + "['" + property + "']";
Object propertyVal = readObjectProperty(property, model, ctx);
if (isLeaf()) {
ctx.addResult(evalPath, propertyVal);
} else {
next().evaluate(evalPath, propertyVal, ctx);
}
} else {
next().evaluate(evalPath, propertyVal, ctx);
if(ctx.configuration().getOptions().contains(Option.MERGE_MULTI_PROPS)) {
String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]";
if (!isLeaf()) {
throw new InvalidPathException("Multi properties can only be used as path leafs: " + evalPath);
} else {
Object map = ctx.jsonProvider().createMap();
for (String property : properties) {
if(hasProperty(property, model, ctx)) {
Object propertyVal = readObjectProperty(property, model, ctx);
ctx.jsonProvider().setProperty(map, property, propertyVal);
}
}
ctx.addResult(evalPath, map);
}
} else {
if (!isLeaf()) {
String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]";
throw new InvalidPathException("Multi properties can only be used as path leafs: " + evalPath);
} else {
for (String property : properties) {
String evalPath = currentPath + "['" + property + "']";
if(hasProperty(property, model, ctx)) {
Object propertyVal = readObjectProperty(property, model, ctx);
ctx.addResult(evalPath, propertyVal);
}
}
}
}
}
}
private boolean hasProperty(String property, Object model, EvaluationContextImpl ctx) {
return ctx.jsonProvider().getPropertyKeys(model).contains(property);
}
private Object readObjectProperty(String property, Object model, EvaluationContextImpl ctx) {
if (ctx.options().contains(Option.THROW_ON_MISSING_PROPERTY) && !ctx.jsonProvider().getPropertyKeys(model).contains(property)) {
throw new PathNotFoundException("Path [" + property + "] not found in the current context:\n" + ctx.jsonProvider().toJson(model));
throw new PathNotFoundException("Path [" + property + "] not found in the current context" );
}
return ctx.jsonProvider().getProperty(model, property);
}

23
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java

@ -1,29 +1,32 @@
package com.jayway.jsonpath.internal.spi.compiler;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.internal.Utils;
import java.util.List;
/**
*
*/
class PropertyPathToken extends PathToken {
private final String property;
private final List<String> properties;
public PropertyPathToken(String property) {
this.property = property;
public PropertyPathToken(List<String> properties) {
this.properties = properties;
}
public String getProperty() {
return property;
public List<String> getProperties() {
return properties;
}
@Override
void evaluate(String currentPath, Object model, EvaluationContextImpl ctx) {
if (!ctx.jsonProvider().isMap(model)) {
throw new PathNotFoundException("Property ['" + property + "'] not found in path " + currentPath);
throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath);
}
handleObjectProperty(currentPath, model, ctx, property);
handleObjectProperty(currentPath, model, ctx, properties);
}
@Override
@ -34,8 +37,8 @@ class PropertyPathToken extends PathToken {
@Override
public String getPathFragment() {
return new StringBuilder()
.append("['")
.append(property)
.append("']").toString();
.append("[")
.append(Utils.join(", ", "'", properties))
.append("]").toString();
}
}

3
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java

@ -86,7 +86,6 @@ class ScanPathToken extends PathToken {
private Predicate createScanPredicate(final PathToken target, final EvaluationContextImpl ctx) {
if (target instanceof PropertyPathToken) {
return new Predicate() {
private PropertyPathToken propertyPathToken = (PropertyPathToken) target;
@ -98,7 +97,7 @@ class ScanPathToken extends PathToken {
@Override
public boolean matches(Object model) {
Collection<String> keys = ctx.jsonProvider().getPropertyKeys(model);
return keys.contains(propertyPathToken.getProperty());
return keys.containsAll(propertyPathToken.getProperties());
}
};
} else if (target instanceof ArrayPathToken) {

4
json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java

@ -1,5 +1,7 @@
package com.jayway.jsonpath.internal.spi.compiler;
import static java.util.Arrays.asList;
/**
*
*/
@ -9,7 +11,7 @@ class WildcardPathToken extends PathToken {
void evaluate(String currentPath, Object model, EvaluationContextImpl ctx) {
if (ctx.jsonProvider().isMap(model)) {
for (String property : ctx.jsonProvider().getPropertyKeys(model)) {
handleObjectProperty(currentPath, model, ctx, property);
handleObjectProperty(currentPath, model, ctx, asList(property));
}
} else if (ctx.jsonProvider().isArray(model)) {
for (int idx = 0; idx < ctx.jsonProvider().length(model); idx++) {

21
json-path/src/main/java/com/jayway/jsonpath/internal/spi/json/JacksonProvider.java

@ -16,6 +16,7 @@ package com.jayway.jsonpath.internal.spi.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.spi.json.Mode;
import org.slf4j.Logger;
@ -25,7 +26,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -34,9 +35,9 @@ public class JacksonProvider extends AbstractJsonProvider {
private static final Logger logger = LoggerFactory.getLogger(JacksonProvider.class);
private ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectReader objectReader = objectMapper.reader().withType(Object.class);
@Override
public Mode getMode() {
return Mode.STRICT;
}
@ -44,7 +45,7 @@ public class JacksonProvider extends AbstractJsonProvider {
@Override
public Object parse(String json) throws InvalidJsonException {
try {
return objectMapper.readValue(json, Object.class);
return objectReader.readValue(json);
} catch (IOException e) {
logger.debug("Invalid JSON: \n" + json);
throw new InvalidJsonException(e);
@ -54,7 +55,7 @@ public class JacksonProvider extends AbstractJsonProvider {
@Override
public Object parse(Reader jsonReader) throws InvalidJsonException {
try {
return objectMapper.readValue(jsonReader, Object.class);
return objectReader.readValue(jsonReader);
} catch (IOException e) {
throw new InvalidJsonException(e);
}
@ -63,7 +64,7 @@ public class JacksonProvider extends AbstractJsonProvider {
@Override
public Object parse(InputStream jsonStream) throws InvalidJsonException {
try {
return objectMapper.readValue(jsonStream, Object.class);
return objectReader.readValue(jsonStream);
} catch (IOException e) {
throw new InvalidJsonException(e);
}
@ -73,9 +74,11 @@ public class JacksonProvider extends AbstractJsonProvider {
public String toJson(Object obj) {
StringWriter writer = new StringWriter();
try {
JsonGenerator jsonGenerator = objectMapper.getJsonFactory().createJsonGenerator(writer);
objectMapper.writeValue(jsonGenerator, obj);
JsonGenerator generator = objectMapper.getFactory().createGenerator(writer);
objectMapper.writeValue(generator, obj);
writer.flush();
writer.close();
generator.close();
return writer.getBuffer().toString();
} catch (IOException e) {
throw new InvalidJsonException();
@ -84,7 +87,7 @@ public class JacksonProvider extends AbstractJsonProvider {
@Override
public Map<String, Object> createMap() {
return new HashMap<String, Object>();
return new LinkedHashMap<String, Object>();
}
@Override

3
json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java

@ -6,10 +6,9 @@ import com.jayway.jsonpath.Configuration;
*
*/
public interface Path {
EvaluationContext evaluate(Object model, Configuration configuration);
boolean isDefinite();
int tokenCount();
}

6
json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProviderFactory.java

@ -14,12 +14,12 @@
*/
package com.jayway.jsonpath.spi.json;
import com.jayway.jsonpath.internal.spi.json.JsonSmartJsonProvider;
import com.jayway.jsonpath.internal.spi.json.JacksonProvider;
public abstract class JsonProviderFactory {
private static JsonProvider provider = new JsonSmartJsonProvider();
//private static JsonProvider provider = new JacksonProvider();
//private static JsonProvider provider = new JsonSmartJsonProvider();
private static JsonProvider provider = new JacksonProvider();
public static JsonProvider createProvider() {
return provider;

4
json-path/src/test/java/com/jayway/jsonpath/Runner.java

@ -42,10 +42,10 @@ public class Runner {
public static void main(String[] args) {
JsonPath jp = JsonPath.compile("$.store.book[3]");
JsonPath jp = JsonPath.compile("$.store.book[*].category");
ReadContext readContext = JsonPath.parse(DOCUMENT);
long start = System.currentTimeMillis();
for (long i = 0; i < 100000; i++) {
for (long i = 0; i < 50000000; i++) {
readContext.read(jp);
}

Loading…
Cancel
Save