/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed 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.google.openrtb.json;

import static com.google.openrtb.json.OpenRtbJsonUtils.writeEnumField;
import static com.google.openrtb.json.OpenRtbJsonUtils.writeEnums;
import static com.google.openrtb.json.OpenRtbJsonUtils.writeIntBoolField;
import static com.google.openrtb.json.OpenRtbJsonUtils.writeStrings;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.openrtb.OpenRtb.NativeRequest;
import com.google.openrtb.OpenRtb.NativeResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Serializes OpenRTB {@link NativeRequest}/{@link NativeResponse} messages to JSON.
 *
 *
Note: Among methods that write to a {@link JsonGenerator} parameter, only the {@code public}
 * methods will call {@code flush()} on the generator before returning.
 *
 *
This class is threadsafe.
 */
public class OpenRtbNativeJsonWriter extends AbstractOpenRtbJsonWriter {
  private OpenRtbJsonWriter coreWriter;

  protected OpenRtbNativeJsonWriter(OpenRtbJsonFactory factory) {
    super(factory);
  }

  /**
   * Serializes a {@link NativeRequest} to JSON, returned as a {@code String}.
   */
  public String writeNativeRequest(NativeRequest req) throws IOException {
    try (StringWriter writer = new StringWriter()) {
      writeNativeRequest(req, writer);
      return writer.toString();
    }
  }

  /**
   * Serializes a {@link NativeRequest} to JSON, streamed into an {@link Writer}.
   *
   * @see JsonFactory#createGenerator(Writer)
   */
  public void writeNativeRequest(NativeRequest req, Writer writer) throws IOException {
    JsonGenerator gen = factory().getJsonFactory().createGenerator(writer);
    writeNativeRequest(req, gen);
  }

  /**
   * Serializes a {@link NativeRequest} to JSON, streamed into an {@link OutputStream}.
   *
   * @see JsonFactory#createGenerator(OutputStream)
   */
  public void writeNativeRequest(NativeRequest req, OutputStream os) throws IOException {
    JsonGenerator gen = factory().getJsonFactory().createGenerator(os);
    writeNativeRequest(req, gen);
  }

  /**
   * Serializes a {@link NativeRequest} to JSON, with a provided {@link JsonGenerator}
   * which allows several choices of output and encoding.
   */
  public final void writeNativeRequest(NativeRequest req, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    if (factory().isRootNativeField()) {
      gen.writeObjectFieldStart("native");
    }
    writeNativeRequestFields(req, gen);
    writeExtensions(req, gen);
    if (factory().isRootNativeField()) {
      gen.writeEndObject();
    }
    gen.writeEndObject();
    gen.flush();
  }

  protected void writeNativeRequestFields(NativeRequest req, JsonGenerator gen) throws IOException {
    if (req.hasVer()) {
      gen.writeStringField("ver", req.getVer());
    }
    if (req.hasLayout()) {
      writeEnumField("layout", req.getLayout(), gen);
    }
    if (req.hasAdunit()) {
      writeEnumField("adunit", req.getAdunit(), gen);
    }
    if (req.hasPlcmtcnt()) {
      gen.writeNumberField("plcmtcnt", req.getPlcmtcnt());
    }
    if (req.hasSeq()) {
      gen.writeNumberField("seq", req.getSeq());
    }
    if (checkRequired(req.getAssetsCount())) {
      gen.writeArrayFieldStart("assets");
      for (NativeRequest.Asset asset : req.getAssetsList()) {
        writeReqAsset(asset, gen);
      }
      gen.writeEndArray();
    }
    if (req.hasContext()) {
      writeEnumField("context", req.getContext(), gen);
    }
    if (req.hasContextsubtype()) {
      writeEnumField("contextsubtype", req.getContextsubtype(), gen);
    }
    if (req.hasPlcmttype()) {
      writeEnumField("plcmttype", req.getPlcmttype(), gen);
    }
    if (req.hasAurlsupport()) {
      writeIntBoolField("aurlsupport", req.getAurlsupport(), gen);
    }
    if (req.hasDurlsupport()) {
      writeIntBoolField("durlsupport", req.getDurlsupport(), gen);
    }
    if (req.getEventtrackersCount() != 0) {
      gen.writeArrayFieldStart("eventtrackers");
      for (NativeRequest.EventTrackers trackers : req.getEventtrackersList()) {
        writeReqEventTrackers(trackers, gen);
      }
      gen.writeEndArray();
    }
    if (req.hasPrivacy()) {
      writeIntBoolField("privacy", req.getPrivacy(), gen);
    }
  }

  public final void writeReqAsset(NativeRequest.Asset asset, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeReqAssetFields(asset, gen);
    writeExtensions(asset, gen);
    gen.writeEndObject();
  }

  protected void writeReqAssetFields(NativeRequest.Asset asset, JsonGenerator gen)
      throws IOException {
    gen.writeNumberField("id", asset.getId());
    if (asset.hasRequired()) {
      writeIntBoolField("required", asset.getRequired(), gen);
    }
    switch (asset.getAssetOneofCase()) {
      case TITLE:
        gen.writeFieldName("title");
        writeReqTitle(asset.getTitle(), gen);
        break;
      case IMG:
        gen.writeFieldName("img");
        writeReqImage(asset.getImg(), gen);
        break;
      case VIDEO:
        gen.writeFieldName("video");
        coreWriter().writeVideo(asset.getVideo(), gen);
        break;
      case DATA:
        gen.writeFieldName("data");
        writeReqData(asset.getData(), gen);
        break;
      case ASSETONEOF_NOT_SET:
        checkRequired(false);
    }
  }

  public final void writeReqTitle(NativeRequest.Asset.Title title, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeReqTitleFields(title, gen);
    writeExtensions(title, gen);
    gen.writeEndObject();
  }

  protected void writeReqTitleFields(NativeRequest.Asset.Title title, JsonGenerator gen)
      throws IOException {
    gen.writeNumberField("len", title.getLen());
  }

  public final void writeReqImage(NativeRequest.Asset.Image image, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeReqImageFields(image, gen);
    writeExtensions(image, gen);
    gen.writeEndObject();
  }

  protected void writeReqImageFields(NativeRequest.Asset.Image image, JsonGenerator gen)
      throws IOException {
    if (image.hasType()) {
      writeEnumField("type", image.getType(), gen);
    }
    if (image.hasW()) {
      gen.writeNumberField("w", image.getW());
    }
    if (image.hasH()) {
      gen.writeNumberField("h", image.getH());
    }
    if (image.hasWmin()) {
      gen.writeNumberField("wmin", image.getWmin());
    }
    if (image.hasHmin()) {
      gen.writeNumberField("hmin", image.getHmin());
    }
    if (checkRequired(image.getMimesCount())) {
      writeStrings("mimes", image.getMimesList(), gen);
    }
  }

  public final void writeReqData(NativeRequest.Asset.Data data, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeReqDataFields(data, gen);
    writeExtensions(data, gen);
    gen.writeEndObject();
  }

  protected void writeReqDataFields(NativeRequest.Asset.Data data, JsonGenerator gen)
      throws IOException {
    writeEnumField("type", data.getType(), gen);
    if (data.hasLen()) {
      gen.writeNumberField("len", data.getLen());
    }
  }

  public final void writeReqEventTrackers(
      NativeRequest.EventTrackers tracker, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeReqEventTrackersFields(tracker, gen);
    writeExtensions(tracker, gen);
    gen.writeEndObject();
  }

  protected void writeReqEventTrackersFields(
      NativeRequest.EventTrackers tracker, JsonGenerator gen) throws IOException {
    writeEnumField("event", tracker.getEvent(), gen);
    writeEnums("methods", tracker.getMethodsList(), gen);
  }

  /**
   * Serializes a {@link NativeResponse} to JSON, returned as a {@link String}.
   */
  public String writeNativeResponse(NativeResponse resp) throws IOException {
    try (StringWriter writer = new StringWriter()) {
      writeNativeResponse(resp, writer);
      return writer.toString();
    }
  }

  /**
   * Serializes a {@link NativeResponse} to JSON, streamed to a {@link OutputStream}.
   *
   * @see JsonFactory#createGenerator(OutputStream)
   */
  public void writeNativeResponse(NativeResponse resp, OutputStream os) throws IOException {
    JsonGenerator gen = factory().getJsonFactory().createGenerator(os);
    writeNativeResponse(resp, gen);
  }

  /**
   * Serializes a {@link NativeResponse} to JSON, streamed to a {@link Writer}.
   *
   * @see JsonFactory#createGenerator(Writer)
   */
  public void writeNativeResponse(NativeResponse resp, Writer writer) throws IOException {
    JsonGenerator gen = factory().getJsonFactory().createGenerator(writer);
    writeNativeResponse(resp, gen);
  }

  /**
   * Serializes a {@link NativeResponse} to JSON, with a provided {@link JsonGenerator}
   * which allows several choices of output and encoding.
   */
  public final void writeNativeResponse(NativeResponse resp, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    if (factory().isRootNativeField()) {
      gen.writeObjectFieldStart("native");
    }
    writeNativeResponseFields(resp, gen);
    writeExtensions(resp, gen);
    if (factory().isRootNativeField()) {
      gen.writeEndObject();
    }
    gen.writeEndObject();
    gen.flush();
  }

  protected void writeNativeResponseFields(NativeResponse resp, JsonGenerator gen)
      throws IOException {
    if (resp.hasVer()) {
      gen.writeStringField("ver", resp.getVer());
    }
    if (resp.getAssetsCount() != 0) {
      gen.writeArrayFieldStart("assets");
      for (NativeResponse.Asset asset : resp.getAssetsList()) {
        writeRespAsset(asset, gen);
      }
      gen.writeEndArray();
    }
    gen.writeFieldName("link");
    writeRespLink(resp.getLink(), gen);
    writeStrings("imptrackers", resp.getImptrackersList(), gen);
    if (resp.hasJstracker()) {
      gen.writeStringField("jstracker", resp.getJstracker());
    }
    if (resp.hasAssetsurl()) {
      gen.writeStringField("assetsurl", resp.getAssetsurl());
    }
    if (resp.hasDcourl()) {
      gen.writeStringField("dcourl", resp.getDcourl());
    }
    if (resp.getEventtrackersCount() != 0) {
      gen.writeArrayFieldStart("eventtrackers");
      for (NativeResponse.EventTracker tracker : resp.getEventtrackersList()) {
        writeRespEventTracker(tracker, gen);
      }
      gen.writeEndArray();
    }
    if (resp.hasPrivacy()) {
      gen.writeStringField("privacy", resp.getPrivacy());
    }
  }

  public final void writeRespAsset(NativeResponse.Asset asset, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespAssetFields(asset, gen);
    writeExtensions(asset, gen);
    gen.writeEndObject();
  }

  protected void writeRespAssetFields(NativeResponse.Asset asset, JsonGenerator gen)
      throws IOException {
    gen.writeNumberField("id", asset.getId());
    if (asset.hasRequired()) {
      writeIntBoolField("required", asset.getRequired(), gen);
    }
    if (asset.hasLink()) {
      gen.writeFieldName("link");
      writeRespLink(asset.getLink(), gen);
    }
    switch (asset.getAssetOneofCase()) {
      case TITLE:
        gen.writeFieldName("title");
        writeRespTitle(asset.getTitle(), gen);
        break;
      case IMG:
        gen.writeFieldName("img");
        writeRespImage(asset.getImg(), gen);
        break;
      case VIDEO:
        gen.writeFieldName("video");
        writeRespVideo(asset.getVideo(), gen);
        break;
      case DATA:
        gen.writeFieldName("data");
        writeRespData(asset.getData(), gen);
        break;
      case ASSETONEOF_NOT_SET:
        checkRequired(false);
    }
  }

  public final void writeRespTitle(NativeResponse.Asset.Title title, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespTitleFields(title, gen);
    writeExtensions(title, gen);
    gen.writeEndObject();
  }

  protected void writeRespTitleFields(NativeResponse.Asset.Title title, JsonGenerator gen)
      throws IOException {
    gen.writeStringField("text", title.getText());
    if (title.hasLen()) {
      gen.writeNumberField("len", title.getLen());
    }
  }

  public final void writeRespImage(NativeResponse.Asset.Image image, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespImageFields(image, gen);
    writeExtensions(image, gen);
    gen.writeEndObject();
  }

  protected void writeRespImageFields(NativeResponse.Asset.Image image, JsonGenerator gen)
      throws IOException {
    gen.writeStringField("url", image.getUrl());
    if (image.hasType()) {
      writeEnumField("type", image.getType(), gen);
    }
    if (image.hasW()) {
      gen.writeNumberField("w", image.getW());
    }
    if (image.hasH()) {
      gen.writeNumberField("h", image.getH());
    }
  }

  public final void writeRespVideo(NativeResponse.Asset.Video video, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespVideoFields(video, gen);
    writeExtensions(video, gen);
    gen.writeEndObject();
  }

  protected void writeRespVideoFields(NativeResponse.Asset.Video video, JsonGenerator gen)
      throws IOException {
    gen.writeStringField("vasttag", video.getVasttag());
  }

  public final void writeRespData(NativeResponse.Asset.Data data, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespDataFields(data, gen);
    writeExtensions(data, gen);
    gen.writeEndObject();
  }

  protected void writeRespDataFields(NativeResponse.Asset.Data data, JsonGenerator gen)
      throws IOException {
    if (data.hasLabel()) {
      gen.writeStringField("label", data.getLabel());
    }
    gen.writeStringField("value", data.getValue());
    if (data.hasType()) {
      writeEnumField("type", data.getType(), gen);
    }
    if (data.hasLen()) {
      gen.writeNumberField("len", data.getLen());
    }
  }

  public final void writeRespLink(NativeResponse.Link link, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeRespLinkFields(link, gen);
    writeExtensions(link, gen);
    gen.writeEndObject();
  }

  protected void writeRespLinkFields(NativeResponse.Link link, JsonGenerator gen)
      throws IOException {
    if (link.hasUrl()) {
      gen.writeStringField("url", link.getUrl());
    }
    writeStrings("clicktrackers", link.getClicktrackersList(), gen);
    if (link.hasFallback()) {
      gen.writeStringField("fallback", link.getFallback());
    }
  }

  public final void writeRespEventTracker(
      NativeResponse.EventTracker tracker, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeRespEventTrackerFields(tracker, gen);
    writeExtensions(tracker, gen);
    gen.writeEndObject();
  }

  protected void writeRespEventTrackerFields(
      NativeResponse.EventTracker tracker, JsonGenerator gen) throws IOException {
    writeEnumField("event", tracker.getEvent(), gen);
    writeEnumField("method", tracker.getMethod(), gen);
    if (tracker.hasUrl()) {
      gen.writeStringField("url", tracker.getUrl());
    }
  }

  protected final OpenRtbJsonWriter coreWriter() {
    if (coreWriter == null) {
      coreWriter = factory().newWriter();
    }
    return coreWriter;
  }
}


/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed 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.google.openrtb.json;

import static com.google.openrtb.json.OpenRtbJsonUtils.endObject;
import static com.google.openrtb.json.OpenRtbJsonUtils.startObject;

import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.google.openrtb.util.OpenRtbUtils;
import com.google.protobuf.GeneratedMessageV3.ExtendableBuilder;
import java.io.IOException;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Desserializes OpenRTB messages from JSON.
 */
public abstract class AbstractOpenRtbJsonReader {
  static final Logger logger = LoggerFactory.getLogger(AbstractOpenRtbJsonReader.class);
  private final OpenRtbJsonFactory factory;

  protected AbstractOpenRtbJsonReader(OpenRtbJsonFactory factory) {
    this.factory = factory;
  }

  public final OpenRtbJsonFactory factory() {
    return factory;
  }

  protected final <EB extends ExtendableBuilder<?, EB>>
  void readOther(EB msg, JsonParser par, String fieldName) throws IOException {
    if ("ext".equals(fieldName)) {
      readExtensions(msg, par);
    } else {
      par.skipChildren();
    }
  }

  /**
   * Read any extensions that may exist in a message.
   *
   * @param msg Builder of a message that may contain extensions
   * @param par The JSON parser, positioned at the "ext" field
   * @param <EB> Type of message builder being constructed
   * @throws IOException any parsing error
   */
  protected final <EB extends ExtendableBuilder<?, EB>>
  void readExtensions(EB msg, JsonParser par) throws IOException {
    @SuppressWarnings("unchecked")
    Set<OpenRtbJsonExtReader<EB>> extReaders = factory.getReaders((Class<EB>) msg.getClass());
    if (extReaders.isEmpty()) {
      par.skipChildren();
      return;
    }

    startObject(par);
    JsonToken tokLast = par.getCurrentToken();
    JsonLocation locLast = par.getCurrentLocation();

    while (true) {
      boolean extRead = false;
      for (OpenRtbJsonExtReader<EB> extReader : extReaders) {
        if (extReader.filter(par)) {
          extReader.read(msg, par);
          JsonToken tokNew = par.getCurrentToken();
          JsonLocation locNew = par.getCurrentLocation();
          boolean advanced = tokNew != tokLast || !locNew.equals(locLast);
          extRead |= advanced;

          if (!endObject(par)) {
            return;
          } else if (advanced && par.getCurrentToken() != JsonToken.FIELD_NAME) {
            tokLast = par.nextToken();
            locLast = par.getCurrentLocation();
          } else {
            tokLast = tokNew;
            locLast = locNew;
          }
        }
      }

      if (!endObject(par)) {
        // Can't rely on this exit condition inside the for loop because no readers may filter.
        return;
      }

      if (!extRead) {
        // No field was consumed by any reader, so we need to skip the field to make progress.
        if (logger.isDebugEnabled()) {
          logger.debug("Extension field not consumed by any reader, skipping: {} @{}:{}",
              par.getCurrentName(), locLast.getLineNr(), locLast.getCharOffset());
        }
        par.nextToken();
        par.skipChildren();
        tokLast = par.nextToken();
        locLast = par.getCurrentLocation();
      }
      // Else loop, try all readers again
    }
  }

  protected final boolean checkEnum(Enum<?> e) {
    if (e == null) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Invalid enumerated value");
      } else {
        return false;
      }
    }
    return true;
  }

  protected final boolean checkContentCategory(String cat) {
    if (OpenRtbUtils.categoryFromName(cat) == null) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Invalid ContentCategory value");
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * Special case for empty-string input. Returning null in non-@Nullable method,
   * but this is non-strict mode anyway.
   */
  protected final boolean emptyToNull(JsonParser par) throws IOException {
    JsonToken token = par.getCurrentToken();
    if (token == null) {
      token = par.nextToken();
    }
    return !factory().isStrict() && token == null;
  }
}


/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed 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.google.openrtb.json;

import static com.google.common.base.Preconditions.checkNotNull;

import com.fasterxml.jackson.core.JsonFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.protobuf.GeneratedMessageV3.ExtendableBuilder;
import com.google.protobuf.Message;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Factory that will create JSON serializer objects:
 *

 *
Core model: {@link OpenRtbJsonWriter} and {@link OpenRtbJsonReader}

 *
Native model: {@link OpenRtbNativeJsonWriter} and {@link OpenRtbNativeJsonReader}

 *

 *
 *
This class is NOT threadsafe. You should use only to configure and create the
 * reader/writer objects, which will be threadsafe.
 */
public class OpenRtbJsonFactory {
  private static final String FIELDNAME_ALL = "*";

  private JsonFactory jsonFactory;
  private boolean strict;
  private boolean rootNativeField;
  private boolean forceNativeAsObject;
  private final SetMultimap<String, OpenRtbJsonExtReader<?>> extReaders;
  private final Map<String, Map<String, Map<String, OpenRtbJsonExtWriter<?>>>> extWriters;

  protected OpenRtbJsonFactory(
      @Nullable JsonFactory jsonFactory,
      boolean strict,
      boolean rootNativeField,
      boolean forceNativeAsObject,
      @Nullable SetMultimap<String, OpenRtbJsonExtReader<?>> extReaders,
      @Nullable Map<String, Map<String, Map<String, OpenRtbJsonExtWriter<?>>>> extWriters) {
    this.jsonFactory = jsonFactory;
    this.strict = strict;
    this.rootNativeField = rootNativeField;
    this.forceNativeAsObject = forceNativeAsObject;
    this.extReaders = extReaders == null ? LinkedHashMultimap.create() : extReaders;
    this.extWriters = extWriters == null ? new LinkedHashMap<>() : extWriters;
  }

  /**
   * This "immutable-clone-constructor" returns an immutable copy of this factory.
   * Subclasses should have a protected constructor that super-calls this and makes
   * immutable copies of their own fields if necessary.  You can then override the
   * methods that create Reader/Writer objects so they use that constructor.
   */
  protected OpenRtbJsonFactory(OpenRtbJsonFactory config) {
    this.jsonFactory = config.getJsonFactory();
    this.strict = config.strict;
    this.rootNativeField = config.rootNativeField;
    this.forceNativeAsObject = config.forceNativeAsObject;
    this.extReaders = ImmutableSetMultimap.copyOf(config.extReaders);
    this.extWriters = ImmutableMap.copyOf(Maps.transformValues(config.extWriters,
        map -> ImmutableMap.copyOf(Maps.transformValues(map, map2 -> ImmutableMap.copyOf(map2)))));
  }

  /**
   * Creates a new factory with default configuration.
   */
  public static OpenRtbJsonFactory create() {
    return new OpenRtbJsonFactory(null, false, false, false, null, null);
  }

  /**
   * Use a specific {@link JsonFactory}. A default factory will created if this is never called.
   */
  public final OpenRtbJsonFactory setJsonFactory(JsonFactory jsonFactory) {
    this.jsonFactory = checkNotNull(jsonFactory);
    return this;
  }

  /**
   * Sets strict mode.
   */
  public final OpenRtbJsonFactory setStrict(boolean strict) {
    this.strict = strict;
    return this;
  }

  /**
   * Sets root native field generation mode.
   */
  public final OpenRtbJsonFactory setRootNativeField(boolean rootNativeField) {
    this.rootNativeField = rootNativeField;
    return this;
  }

  /**
   * Sets object native field generation mode.
   */
  public final OpenRtbJsonFactory setForceNativeAsObject(boolean forceNativeAsObject) {
    this.forceNativeAsObject = forceNativeAsObject;
    return this;
  }

  /**
   * Returns {@code true} for strict mode, {@code false} lenient mode.
   */
  public final boolean isStrict() {
    return strict;
  }

  /**
   * Returns {@code true} for root native field mode, {@code false} if not.
   */
  public final boolean isRootNativeField() {
    return rootNativeField;
  }

  /**
   * Returns {@code true} for object native field mode, {@code false} if not.
   */
  public boolean isForceNativeAsObject() {
    return forceNativeAsObject;
  }

  /**
   * Register an extension reader.
   *
   * @param extReader code to desserialize some extension properties
   * @param msgKlass class of extension message's builder, e.g. {@code MyImp.Builder.class}
   */
  public final <EB extends ExtendableBuilder<?, EB>> OpenRtbJsonFactory register(
      OpenRtbJsonExtReader<EB> extReader, Class<EB> msgKlass) {
    extReaders.put(msgKlass.getName(), extReader);
    return this;
  }

  /**
   * Register an extension writer, bound to a specific field name. This writer will be
   * used in preference to a non-field-specific writer that may exist for the same class.
   *
   * @param extWriter code to serialize some {@code extKlass}'s properties
   * @param extKlass class of extension message, e.g. {@code MyImp.class}
   * @param msgKlass class of container message, e.g. {@code Imp.class}
   * @param fieldName name of the field containing the extension
   * @param <T> Type of value for the extension
   * @see #register(OpenRtbJsonExtWriter, Class, Class)
   */
  public final <T> OpenRtbJsonFactory register(OpenRtbJsonExtWriter<T> extWriter,
      Class<T> extKlass, Class<? extends Message> msgKlass, String fieldName) {
    Map<String, Map<String, OpenRtbJsonExtWriter<?>>> mapMsg = extWriters.get(msgKlass.getName());
    if (mapMsg == null) {
      extWriters.put(msgKlass.getName(), mapMsg = new LinkedHashMap<>());
    }
    Map<String, OpenRtbJsonExtWriter<?>> mapKlass = mapMsg.get(extKlass.getName());
    if (mapKlass == null) {
      mapMsg.put(extKlass.getName(), mapKlass = new LinkedHashMap<>());
    }
    mapKlass.put(fieldName == null ? FIELDNAME_ALL : fieldName, extWriter);
    return this;
  }

  /**
   * Register an extension writer, not bound to any a field name (so this serializer
   * can be used for any extension of the provided class).
   *
   * @param extWriter code to serialize some {@code extKlass}'s properties
   * @param extKlass class of extension message, e.g. {@code MyImp.class}
   * @param msgKlass class of container message, e.g. {@code Imp.class}
   * @param <T> Type of value for the extension
   * @see #register(OpenRtbJsonExtWriter, Class, Class, String)
   */
  public final <T> OpenRtbJsonFactory register(OpenRtbJsonExtWriter<T> extWriter,
      Class<T> extKlass, Class<? extends Message> msgKlass) {
    return register(extWriter, extKlass, msgKlass, FIELDNAME_ALL);
  }

  /**
   * Creates an {@link OpenRtbJsonWriter}, configured to the current state of this factory.
   */
  public OpenRtbJsonWriter newWriter() {
    return new OpenRtbJsonWriter(new OpenRtbJsonFactory(this));
  }

  /**
   * Creates an {@link OpenRtbJsonReader}, configured to the current state of this factory.
   */
  public OpenRtbJsonReader newReader() {
    return new OpenRtbJsonReader(new OpenRtbJsonFactory(this));
  }

  /**
   * Creates an {@link OpenRtbNativeJsonWriter}, configured to the current state of this factory.
   */
  public OpenRtbNativeJsonWriter newNativeWriter() {
    return new OpenRtbNativeJsonWriter(new OpenRtbJsonFactory(this));
  }

  /**
   * Creates an {@link OpenRtbNativeJsonReader}, configured to the current state of this factory.
   */
  public OpenRtbNativeJsonReader newNativeReader() {
    return new OpenRtbNativeJsonReader(new OpenRtbJsonFactory(this));
  }

  @SuppressWarnings("unchecked")
  final <EB extends ExtendableBuilder<?, EB>>
      Set<OpenRtbJsonExtReader<EB>> getReaders(Class<EB> msgClass) {
    return (Set<OpenRtbJsonExtReader<EB>>) (Set<?>) extReaders.get(msgClass.getName());
  }

  @SuppressWarnings("unchecked")
  final <T> OpenRtbJsonExtWriter<T> getWriter(
      Class<? extends Message> msgClass, Class<?> extClass, @Nullable String fieldName) {
    Map<String, Map<String, OpenRtbJsonExtWriter<?>>> mapMsg = extWriters.get(msgClass.getName());
    if (mapMsg == null) {
      return null;
    }
    Map<String, OpenRtbJsonExtWriter<?>> mapKlass = mapMsg.get(extClass.getName());
    if (mapKlass == null) {
      return null;
    }
    if (fieldName != null && !FIELDNAME_ALL.equals(fieldName)) {
      OpenRtbJsonExtWriter<T> writer = (OpenRtbJsonExtWriter<T>) mapKlass.get(fieldName);
      if (writer != null) {
        return writer;
      }
    }
    return (OpenRtbJsonExtWriter<T>) mapKlass.get(FIELDNAME_ALL);
  }

  /**
   * Returns the {@link JsonFactory} configured for this {@link OpenRtbJsonFactory}.
   * If you didn't set any value with {@link #setJsonFactory(JsonFactory)},
   * will create a default factory.
   */
  public final JsonFactory getJsonFactory() {
    if (jsonFactory == null) {
      jsonFactory = new JsonFactory();
    }
    return jsonFactory;
  }
}


/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed 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.google.openrtb.json;

import static com.google.openrtb.json.OpenRtbJsonUtils.endArray;
import static com.google.openrtb.json.OpenRtbJsonUtils.endObject;
import static com.google.openrtb.json.OpenRtbJsonUtils.getCurrentName;
import static com.google.openrtb.json.OpenRtbJsonUtils.startArray;
import static com.google.openrtb.json.OpenRtbJsonUtils.startObject;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.google.common.io.CharSource;
import com.google.common.io.Closeables;
import com.google.openrtb.OpenRtb.AdUnitId;
import com.google.openrtb.OpenRtb.ContextSubtype;
import com.google.openrtb.OpenRtb.ContextType;
import com.google.openrtb.OpenRtb.DataAssetType;
import com.google.openrtb.OpenRtb.EventTrackingMethod;
import com.google.openrtb.OpenRtb.EventType;
import com.google.openrtb.OpenRtb.ImageAssetType;
import com.google.openrtb.OpenRtb.LayoutId;
import com.google.openrtb.OpenRtb.NativeRequest;
import com.google.openrtb.OpenRtb.NativeResponse;
import com.google.openrtb.OpenRtb.PlacementType;
import com.google.openrtb.util.ProtoUtils;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;

/**
 * Desserializes OpenRTB {@link NativeRequest}/{@link NativeResponse} messages from JSON.
 *
 * <p>This class is threadsafe.
 */
public class OpenRtbNativeJsonReader extends AbstractOpenRtbJsonReader {
  private OpenRtbJsonReader coreReader;

  protected OpenRtbNativeJsonReader(OpenRtbJsonFactory factory) {
    super(factory);
  }

  /**
   * Desserializes a {@link NativeRequest} from a JSON string, provided as a {@link ByteString}.
   */
  public NativeRequest readNativeRequest(ByteString bs) throws IOException {
    return readNativeRequest(bs.newInput());
  }

  /**
   * Desserializes a {@link NativeRequest} from a JSON string, provided as a {@link CharSequence}.
   */
  public NativeRequest readNativeRequest(CharSequence chars) throws IOException {
    return readNativeRequest(CharSource.wrap(chars).openStream());
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, streamed from a {@link Reader}.
   */
  public NativeRequest readNativeRequest(Reader reader) throws IOException {
    return ProtoUtils.built(readNativeRequest(factory().getJsonFactory().createParser(reader)));
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, streamed from an {@link InputStream}.
   */
  public NativeRequest readNativeRequest(InputStream is) throws IOException {
    try {
      return ProtoUtils.built(readNativeRequest(factory().getJsonFactory().createParser(is)));
    } finally {
      Closeables.closeQuietly(is);
    }
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, with a provided {@link JsonParser}
   * which allows several choices of input and encoding.
   */
  public final NativeRequest.Builder readNativeRequest(JsonParser par) throws IOException {
    if (emptyToNull(par)) {
      return null;
    }
    NativeRequest.Builder req = NativeRequest.newBuilder();
    boolean rootNativeField = false;
    boolean firstField = true;
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        if (firstField) {
          firstField = false;
          if ((rootNativeField = "native".equals(fieldName)) == true) {
            startObject(par);
            fieldName = getCurrentName(par);
            par.nextToken();
          }
        }
        if (par.getCurrentToken() != JsonToken.VALUE_NULL) {
          readNativeRequestField(par, req, fieldName);
        }
      }
    }
    if (rootNativeField && !endObject(par)) {
      par.nextToken();
    }
    return req;
  }

  protected void readNativeRequestField(JsonParser par, NativeRequest.Builder req, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "ver":
        req.setVer(par.getText());
        break;
      case "layout": {
          LayoutId value = LayoutId.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setLayout(value);
          }
        }
        break;
      case "adunit": {
          AdUnitId value = AdUnitId.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setAdunit(value);
          }
        }
        break;
      case "plcmtcnt":
        req.setPlcmtcnt(par.getIntValue());
        break;
      case "seq":
        req.setSeq(par.getIntValue());
        break;
      case "assets":
        for (startArray(par); endArray(par); par.nextToken()) {
          req.addAssets(readReqAsset(par));
        }
        break;
      case "context": {
          ContextType value = ContextType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setContext(value);
          }
        }
        break;
      case "contextsubtype": {
          ContextSubtype value = ContextSubtype.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setContextsubtype(value);
          }
        }
        break;
      case "plcmttype": {
          PlacementType value = PlacementType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setPlcmttype(value);
          }
        }
        break;
      case "aurlsupport":
        req.setAurlsupport(par.getValueAsBoolean());
        break;
      case "durlsupport":
        req.setDurlsupport(par.getValueAsBoolean());
        break;
      case "privacy":
        req.setPrivacy(par.getValueAsBoolean());
        break;
      case "eventtrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          req.addEventtrackers(readReqEventTrackers(par));
        }
        break;
      default:
        readOther(req, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Builder readReqAsset(JsonParser par) throws IOException {
    NativeRequest.Asset.Builder asset = NativeRequest.Asset.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqAssetField(par, asset, fieldName);
      }
    }
    return asset;
  }

  protected void readReqAssetField(
      JsonParser par, NativeRequest.Asset.Builder asset, String fieldName) throws IOException {
    switch (fieldName) {
      case "id":
        asset.setId(par.getIntValue());
        break;
      case "required":
        asset.setRequired(par.getValueAsBoolean());
        break;
      case "title":
        asset.setTitle(readReqTitle(par));
        break;
      case "img":
        asset.setImg(readReqImage(par));
        break;
      case "video":
        asset.setVideo(coreReader().readVideo(par));
        break;
      case "data":
        asset.setData(readReqData(par));
        break;
      default:
        readOther(asset, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Title.Builder readReqTitle(JsonParser par)
      throws IOException {
    NativeRequest.Asset.Title.Builder title = NativeRequest.Asset.Title.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqTitleField(par, title, fieldName);
      }
    }
    return title;
  }

  protected void readReqTitleField(
      JsonParser par, NativeRequest.Asset.Title.Builder title, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "len":
        title.setLen(par.getIntValue());
        break;
      default:
        readOther(title, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Image.Builder readReqImage(JsonParser par)
      throws IOException {
    NativeRequest.Asset.Image.Builder req = NativeRequest.Asset.Image.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqImageField(par, req, fieldName);
      }
    }
    return req;
  }

  protected void readReqImageField(
      JsonParser par, NativeRequest.Asset.Image.Builder image, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "type": {
          ImageAssetType value = ImageAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            image.setType(value);
          }
        }
        break;
      case "w":
        image.setW(par.getIntValue());
        break;
      case "h":
        image.setH(par.getIntValue());
        break;
      case "wmin":
        image.setWmin(par.getIntValue());
        break;
      case "hmin":
        image.setHmin(par.getIntValue());
        break;
      case "mimes":
        for (startArray(par); endArray(par); par.nextToken()) {
          image.addMimes(par.getText());
        }
        break;
      default:
        readOther(image, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Data.Builder readReqData(JsonParser par) throws IOException {
    NativeRequest.Asset.Data.Builder data = NativeRequest.Asset.Data.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqDataField(par, data, fieldName);
      }
    }
    return data;
  }

  protected void readReqDataField(
      JsonParser par, NativeRequest.Asset.Data.Builder data, String fieldName) throws IOException {
    switch (fieldName) {
      case "type": {
          DataAssetType value = DataAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            data.setType(value);
          }
        }
        break;
      case "len":
        data.setLen(par.getIntValue());
        break;
      default:
        readOther(data, par, fieldName);
    }
  }

  public final NativeRequest.EventTrackers.Builder readReqEventTrackers(JsonParser par)
      throws IOException {
    NativeRequest.EventTrackers.Builder trackers = NativeRequest.EventTrackers.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqEventTrackersField(par, trackers, fieldName);
      }
    }
    return trackers;
  }

  protected void readReqEventTrackersField(
      JsonParser par, NativeRequest.EventTrackers.Builder trackers, String fieldName)
          throws IOException {
    switch (fieldName) {
      case "event": {
          EventType value = EventType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            trackers.setEvent(value);
          }
        }
        break;
      case "methods":
        for (startArray(par); endArray(par); par.nextToken()) {
          EventTrackingMethod value = EventTrackingMethod.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            trackers.addMethods(value);
          }
        }
        break;
      default:
        readOther(trackers, par, fieldName);
    }
  }

  /**
   * Desserializes a {@link NativeResponse} from a JSON string, provided as a {@link ByteString}.
   */
  public NativeResponse readNativeResponse(ByteString bs) throws IOException {
    return readNativeResponse(bs.newInput());
  }

  /**
   * Desserializes a {@link NativeResponse} from a JSON string, provided as a {@link CharSequence}.
   */
  public NativeResponse readNativeResponse(CharSequence chars) throws IOException {
    return readNativeResponse(CharSource.wrap(chars).openStream());
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, streamed from a {@link Reader}.
   */
  public NativeResponse readNativeResponse(Reader reader) throws IOException {
    return ProtoUtils.built(readNativeResponse(factory().getJsonFactory().createParser(reader)));
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, streamed from an {@link InputStream}.
   */
  public NativeResponse readNativeResponse(InputStream is) throws IOException {
    try {
      return ProtoUtils.built(readNativeResponse(factory().getJsonFactory().createParser(is)));
    } finally {
      Closeables.closeQuietly(is);
    }
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, with a provided {@link JsonParser}
   * which allows several choices of input and encoding.
   */
  public final NativeResponse.Builder readNativeResponse(JsonParser par) throws IOException {
    if (emptyToNull(par)) {
      return null;
    }
    NativeResponse.Builder resp = NativeResponse.newBuilder();
    boolean rootNativeField = false;
    boolean firstField = true;
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        if (firstField) {
          firstField = false;
          if ((rootNativeField = "native".equals(fieldName)) == true) {
            startObject(par);
            fieldName = getCurrentName(par);
            par.nextToken();
          }
        }
        if (par.getCurrentToken() != JsonToken.VALUE_NULL) {
          readNativeResponseField(par, resp, fieldName);
        }
      }
    }
    if (rootNativeField && !endObject(par)) {
      par.nextToken();
    }
    return resp;
  }

  protected void readNativeResponseField(
      JsonParser par, NativeResponse.Builder resp, String fieldName) throws IOException {
    switch (fieldName) {
      case "ver":
        resp.setVer(par.getText());
        break;
      case "assets":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addAssets(readRespAsset(par));
        }
        break;
      case "link":
        resp.setLink(readRespLink(par));
        break;
      case "imptrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addImptrackers(par.getText());
        }
        break;
      case "jstracker":
        resp.setJstracker(par.getText());
        break;
      case "assetsurl":
        resp.setAssetsurl(par.getText());
        break;
      case "dcourl":
        resp.setDcourl(par.getText());
        break;
      case "eventtrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addEventtrackers(readRespEventTracker(par));
        }
        break;
      case "privacy":
        resp.setPrivacy(par.getText());
        break;
      default:
        readOther(resp, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Builder readRespAsset(JsonParser par) throws IOException {
    NativeResponse.Asset.Builder asset = NativeResponse.Asset.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespAssetField(par, asset, fieldName);
      }
    }
    return asset;
  }

  protected void readRespAssetField(
      JsonParser par, NativeResponse.Asset.Builder asset, String fieldName) throws IOException {
    switch (fieldName) {
      case "id":
        asset.setId(par.getIntValue());
        break;
      case "required":
        asset.setRequired(par.getValueAsBoolean());
        break;
      case "title":
        asset.setTitle(readRespTitle(par));
        break;
      case "img":
        asset.setImg(readRespImage(par));
        break;
      case "video":
        asset.setVideo(readRespVideo(par));
        break;
      case "data":
        asset.setData(readRespData(par));
        break;
      case "link":
        asset.setLink(readRespLink(par));
        break;
      default:
        readOther(asset, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Title.Builder readRespTitle(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Title.Builder title = NativeResponse.Asset.Title.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespTitleField(par, title, fieldName);
      }
    }
    return title;
  }

  protected void readRespTitleField(
      JsonParser par, NativeResponse.Asset.Title.Builder title, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "text":
        title.setText(par.getText());
        break;
      case "len":
        title.setLen(par.getIntValue());
        break;
      default:
        readOther(title, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Image.Builder readRespImage(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Image.Builder image = NativeResponse.Asset.Image.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespImageField(par, image, fieldName);
      }
    }
    return image;
  }

  protected void readRespImageField(
      JsonParser par, NativeResponse.Asset.Image.Builder image, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "url":
        image.setUrl(par.getText());
        break;
      case "type": {
          ImageAssetType value = ImageAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            image.setType(value);
          }
        }
        break;
      case "w":
        image.setW(par.getIntValue());
        break;
      case "h":
        image.setH(par.getIntValue());
        break;
      default:
        readOther(image, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Video.Builder readRespVideo(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Video.Builder video = NativeResponse.Asset.Video.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespVideoField(par, video, fieldName);
      }
    }
    return video;
  }

  protected void readRespVideoField(
      JsonParser par, NativeResponse.Asset.Video.Builder video, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "vasttag":
        video.setVasttag(par.getText());
        break;
      default:
        readOther(video, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Data.Builder readRespData(JsonParser par) throws IOException {
    NativeResponse.Asset.Data.Builder data = NativeResponse.Asset.Data.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespDataField(par, data, fieldName);
      }
    }
    return data;
  }

  protected void readRespDataField(
      JsonParser par, NativeResponse.Asset.Data.Builder data, String fieldName) throws IOException {
    switch (fieldName) {
      case "label":
        data.setLabel(par.getText());
        break;
      case "value":
        data.setValue(par.getText());
        break;
      case "type": {
          DataAssetType value = DataAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            data.setType(value);
          }
        }
        break;
      case "len":
        data.setLen(par.getIntValue());
        break;
      default:
        readOther(data, par, fieldName);
    }
  }

  public final NativeResponse.Link.Builder readRespLink(JsonParser par) throws IOException {
    NativeResponse.Link.Builder link = NativeResponse.Link.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespLinkField(par, link, fieldName);
      }
    }
    return link;
  }

  protected void readRespLinkField(
      JsonParser par, NativeResponse.Link.Builder link, String fieldName) throws IOException {
    switch (fieldName) {
      case "url":
        link.setUrl(par.getText());
        break;
      case "clicktrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          link.addClicktrackers(par.getText());
        }
        break;
      case "fallback":
        link.setFallback(par.getText());
        break;
      default:
        readOther(link, par, fieldName);
    }
  }

  public final NativeResponse.EventTracker.Builder readRespEventTracker(JsonParser par)
      throws IOException {
    NativeResponse.EventTracker.Builder tracker = NativeResponse.EventTracker.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespEventTrackerField(par, tracker, fieldName);
      }
    }
    return tracker;
  }

  protected void readRespEventTrackerField(
      JsonParser par, NativeResponse.EventTracker.Builder tracker, String fieldName)
          throws IOException {
    switch (fieldName) {
      case "event": {
          EventType value = EventType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            tracker.setEvent(value);
          }
        }
        break;
      case "method": {
          EventTrackingMethod value = EventTrackingMethod.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            tracker.setMethod(value);
          }
        }
        break;
      case "url":
        tracker.setUrl(par.getText());
        break;
      default:
        readOther(tracker, par, fieldName);
    }
  }

  protected final OpenRtbJsonReader coreReader() {
    if (coreReader == null) {
      coreReader = factory().newReader();
    }
    return coreReader;
  }
}


/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed 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.google.openrtb.snippet;

import com.google.common.collect.ImmutableList;
import com.google.openrtb.OpenRtb.BidRequest.ImpOrBuilder;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid.Bid;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid.BidOrBuilder;
import com.google.openrtb.OpenRtb.BidResponse.SeatBidOrBuilder;
import java.util.List;
import javax.inject.Singleton;

/**
 * Default {@link SnippetProcessor}.
 */
@Singleton
public class OpenRtbSnippetProcessor extends SnippetProcessor {
  public static final OpenRtbSnippetProcessor ORTB_NULL = new OpenRtbSnippetProcessor(false) {
    @Override public String process(SnippetProcessorContext ctx, String snippet) {
      return SnippetProcessor.NULL.process(ctx, snippet);
    }};

  private final boolean extendedFields;

  /**
   * Creates a processor.
   *
   * @param extendedFields {@code true} will support macro substitution in all {@link Bid}
   *     fields of string type, not only the {@code adm} field mandated by the spec
   */
  public OpenRtbSnippetProcessor(boolean extendedFields) {
    this.extendedFields = extendedFields;
  }

  /**
   * Creates a processor.
   */
  public OpenRtbSnippetProcessor() {
    this(false);
  }

  @Override protected List<SnippetMacroType> registerMacros() {
    return ImmutableList.<SnippetMacroType>copyOf(OpenRtbMacros.values());
  }

  @Override protected boolean processMacroAt(
      SnippetProcessorContext ctx, SnippetMacroType macroDef) {
    if (macroDef instanceof OpenRtbMacros) {
      switch ((OpenRtbMacros) macroDef) {
        // Standard OpenRTB macros (OpenRTB 4.6) ----------------------------

        case AUCTION_AD_ID: {
          if (ctx.getBid().hasAdid()) {
            ctx.builder().append(ctx.getBid().getAdid());
          }
          return true;
        }

        case AUCTION_BID_ID: {
          if (ctx.response().hasBidid()) {
            ctx.builder().append(ctx.response().getBidid());
          }
          return true;
        }

        case AUCTION_CURRENCY: {
          if (ctx.request().getCurCount() == 1) {
            ctx.builder().append(ctx.request().getCur(0));
          }
          return true;
        }

        case AUCTION_ID: {
          ctx.builder().append(ctx.request().getId());
          return true;
        }

        case AUCTION_IMP_ID: {
          ctx.builder().append(findImp(ctx, macroDef).getId());
          return true;
        }

        case AUCTION_SEAT_ID: {
          SeatBidOrBuilder seatBid = findSeat(ctx, macroDef);
          if (seatBid.hasSeat()) {
            ctx.builder().append(seatBid.getSeat());
          }
          return true;
        }

        // "Server-side" macros: keep the macro untouched, so it will be
        // expanded by the exchange or other system that receives the message.
        case AUCTION_PRICE:
        case AUCTION_MBR:
        case AUCTION_LOSS:
          ctx.builder().append(((OpenRtbMacros) macroDef).key());
          return true;
      }
    }

    return false;
  }

  /**
   * Processes the context's response in-place, modifying properties that may contain macros.
   */
  public void process(SnippetProcessorContext bidCtx) {
    for (SeatBid.Builder seat : bidCtx.response().getSeatbidBuilderList()) {
      for (Bid.Builder bid : seat.getBidBuilderList()) {
        bidCtx.setBid(bid);
        processFields(bidCtx);
      }
    }
  }

  /**
   * Processes all fields of a bid that should support macro expansion.
   */
  protected void processFields(SnippetProcessorContext bidCtx) {
    Bid.Builder bid = bidCtx.getBid();

    // Properties that can also be in the RHS of macros used by other properties.

    if (extendedFields) {
      if (bid.hasAdid()) {
        bid.setAdid(process(bidCtx, bid.getAdid()));
      }
      bid.setId(process(bidCtx, bid.getId()));
    }

    // Properties that are NOT the RHS of any macro.

    if (bid.hasAdm()) {
      bid.setAdm(process(bidCtx, bid.getAdm()));
    }

    if (extendedFields) {
      if (bid.hasBurl()) {
        bid.setBurl(process(bidCtx, bid.getBurl()));
      }
      if (bid.hasCid()) {
        bid.setCid(process(bidCtx, bid.getCid()));
      }
      if (bid.hasCrid()) {
        bid.setCrid(process(bidCtx, bid.getCrid()));
      }
      if (bid.hasDealid()) {
        bid.setDealid(process(bidCtx, bid.getDealid()));
      }
      bid.setImpid(process(bidCtx, bid.getImpid()));
      if (bid.hasIurl()) {
        bid.setIurl(process(bidCtx, bid.getIurl()));
      }
      if (bid.hasLurl()) {
        bid.setIurl(process(bidCtx, bid.getLurl()));
      }
      if (bid.hasNurl()) {
        bid.setNurl(process(bidCtx, bid.getNurl()));
      }
    }
  }

  private SeatBidOrBuilder findSeat(SnippetProcessorContext ctx, SnippetMacroType macro) {
    for (SeatBidOrBuilder seatBid : ctx.response().getSeatbidOrBuilderList()) {
      for (BidOrBuilder lookupBid : seatBid.getBidOrBuilderList()) {
        if (lookupBid == ctx.getBid()) {
          return seatBid;
        }
      }
    }

    throw new UndefinedMacroException(
        macro, "Bid doesn't belong to this request");
  }

  protected ImpOrBuilder findImp(SnippetProcessorContext ctx, SnippetMacroType macro) {
    for (ImpOrBuilder imp : ctx.request().getImpOrBuilderList()) {
      if (imp.getId().equals(ctx.getBid().getImpid())) {
        return imp;
      }
    }

    throw new UndefinedMacroException(macro,
        "Bid's impression id: " + ctx.getBid().getImpid() + " doesn't match request");
  }
}