/*
* Copyright 2017 StreamSets Inc.
*
* Licensed under 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 com.streamsets.datacollector.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.streamsets.pipeline.api.ext.io.CountingReader;
import com.streamsets.pipeline.api.ext.io.OverrunReader;
import com.streamsets.pipeline.api.ext.json.Mode;
import com.streamsets.pipeline.api.impl.Utils;
import com.streamsets.pipeline.api.ext.io.ObjectLengthException;
import com.streamsets.pipeline.api.ext.io.OverrunException;
import com.streamsets.pipeline.lib.util.ExceptionUtils;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
public class OverrunJsonObjectReaderImpl extends JsonObjectReaderImpl {
private static final ObjectMapper DEFAULT_OVERRUN_OBJECT_MAPPER = new ObjectMapper();
private static final ThreadLocal<OverrunJsonObjectReaderImpl> TL = new ThreadLocal<>();
private final OverrunReader countingReader;
private final int maxObjectLen;
private long startOffset;
private long limitOffset;
private boolean overrun;
static {
SimpleModule module = new SimpleModule();
module.addDeserializer(Map.class, new MapDeserializer());
module.addDeserializer(List.class, new ListDeserializer());
DEFAULT_OVERRUN_OBJECT_MAPPER.registerModule(module);
DEFAULT_OVERRUN_OBJECT_MAPPER.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
DEFAULT_OVERRUN_OBJECT_MAPPER.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
}
public static class EnforcerMap extends LinkedHashMap {
@Override
@SuppressWarnings("unchecked")
public Object put(Object key, Object value) {
try {
return super.put(key, value);
} finally {
checkIfLengthExceededForObjectRead(this);
}
}
@Override
@SuppressWarnings("unchecked")
public String toString() {
StringBuilder sb = new StringBuilder("{");
String separator = "";
for (Map.Entry entry : (Set<Map.Entry>) entrySet()) {
sb.append(separator).append(entry.getKey()).append("=").append(entry.getValue());
if (sb.length() > 100) {
sb.append(", ...");
break;
}
separator = ", ";
}
sb.append("}");
return sb.toString();
}
}
public static class EnforcerList extends ArrayList {
@Override
@SuppressWarnings("unchecked")
public boolean add(Object o) {
try {
return super.add(o);
} finally {
checkIfLengthExceededForObjectRead(this);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
String separator = "";
for (Object value : this) {
sb.append(separator).append(value);
if (sb.length() > 100) {
sb.append(", ...");
break;
}
separator = ", ";
}
sb.append("]");
return sb.toString();
}
}
private static class MapDeserializer extends JsonDeserializer<Map> {
@Override
public Map deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
return jp.readValueAs(EnforcerMap.class);
}
}
private static class ListDeserializer extends JsonDeserializer<List> {
@Override
public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
return jp.readValueAs(EnforcerList.class);
}
}
public OverrunJsonObjectReaderImpl(Reader reader, long initialPosition, int maxObjectLen, Mode mode) throws IOException {
this(reader, initialPosition, maxObjectLen, mode, Object.class);
}
public OverrunJsonObjectReaderImpl(Reader reader, long initialPosition, int maxObjectLen, Mode mode, Class<?> objectClass) throws IOException {
this(reader, initialPosition, maxObjectLen, mode, objectClass, DEFAULT_OVERRUN_OBJECT_MAPPER);
}
public OverrunJsonObjectReaderImpl(Reader reader, long initialPosition, int maxObjectLen, Mode mode, Class<?> objectClass, ObjectMapper objectMapper) throws IOException {
super(reader, initialPosition, mode, objectClass, objectMapper);
countingReader = (OverrunReader) getReader();
countingReader.setEnabled(true);
this.maxObjectLen = maxObjectLen;
}
@Override
protected Object readObjectFromArray() throws IOException {
Utils.checkState(!overrun, "The underlying input stream had an overrun, the parser is not usable anymore");
countingReader.resetCount();
startOffset = getJsonParser().getCurrentLocation().getCharOffset();
limitOffset = startOffset + maxObjectLen;
try {
TL.set(this);
return super.readObjectFromArray();
} catch (Exception ex) {
ObjectLengthException olex = ExceptionUtils.findSpecificCause(ex, ObjectLengthException.class);
if (olex != null) {
JsonParser parser = getJsonParser();
JsonToken token = parser.getCurrentToken();
if (token == null) {
token = parser.nextToken();
}
while (token != null && parser.getParsingContext() != getRootContext()) {
token = parser.nextToken();
}
throw olex;
} else {
OverrunException oex = ExceptionUtils.findSpecificCause(ex, OverrunException.class);
if (oex != null) {
overrun = true;
throw oex;
}
throw ex;
}
} finally {
TL.remove();
}
}
@Override
protected Object readObjectFromStream() throws IOException {
Utils.checkState(!overrun, "The underlying input stream had an overrun, the parser is not usable anymore");
countingReader.resetCount();
limitOffset = getJsonParser().getCurrentLocation().getCharOffset() + maxObjectLen;
try {
TL.set(this);
return super.readObjectFromStream();
} catch (Exception ex) {
ObjectLengthException olex = ExceptionUtils.findSpecificCause(ex, ObjectLengthException.class);
if (olex != null) {
fastForwardToNextRootObject();
throw olex;
} else {
OverrunException oex = ExceptionUtils.findSpecificCause(ex, OverrunException.class);
if (oex != null) {
overrun = true;
throw oex;
}
throw ex;
}
} finally {
TL.remove();
}
}
private static void checkIfLengthExceededForObjectRead(Object json) {
OverrunJsonObjectReaderImpl enforcer = TL.get();
if (checkNotNull(enforcer, "Enforcer was null").maxObjectLen > -1) {
if (enforcer.getJsonParser().getCurrentLocation().getCharOffset() > enforcer.limitOffset) {
ExceptionUtils.throwUndeclared(new ObjectLengthException(Utils.format(
"JSON Object at offset '{}' exceeds max length '{}'", enforcer.startOffset, enforcer.maxObjectLen),
enforcer.startOffset));
}
}
}
@Override
protected void fastForwardLeaseReader() {
((CountingReader) getReader()).resetCount();
}
}