package com.googlecode.objectify.impl;

import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.condition.Always;
import com.googlecode.objectify.condition.If;
import com.googlecode.objectify.condition.InitializeIf;
import com.googlecode.objectify.repackaged.gentyref.GenericTypeReflector;

import java.lang.reflect.Field;
import java.lang.reflect.Type;

/**
 * Utility that makes it easy to generate If conditions.
 */
public class IfConditionGenerator
{
	/** For simple cases that always test positive (ie, empty if arrays) */
	private static final If<?, ?>[] ALWAYS = new If<?, ?>[] { new Always() };

	/** */
	ObjectifyFactory fact;

	/**
	 */
	public IfConditionGenerator(ObjectifyFactory fact) {
		this.fact = fact;
	}

	/**
	 * Clever enough to recognize that an empty set of conditions means Always.
	 */
	public If<?, ?>[] generateIfConditions(Class<? extends If<?, ?>>[] ifClasses, Field field) {
		if (ifClasses.length == 0)
			return ALWAYS;

		If<?, ?>[] result = new If<?, ?>[ifClasses.length];

		for (int i=0; i<ifClasses.length; i++) {

			Class<? extends If<?, ?>> ifClass = ifClasses[i];
			result[i] = this.createIf(ifClass, field);

			// Sanity check the generic If class types to ensure that they match the actual types of the field & entity.

			Type valueType = GenericTypeReflector.getTypeParameter(ifClass, If.class.getTypeParameters()[0]);
			Class<?> valueClass = GenericTypeReflector.erase(valueType);

			Type pojoType = GenericTypeReflector.getTypeParameter(ifClass, If.class.getTypeParameters()[1]);
			Class<?> pojoClass = GenericTypeReflector.erase(pojoType);

			if (!TypeUtils.isAssignableFrom(valueClass, field.getType()))
				throw new IllegalStateException("Cannot use If class " + ifClass.getName() + " on " + field
						+ " because you cannot assign " + field.getType().getName() + " to " + valueClass.getName());

			if (!TypeUtils.isAssignableFrom(pojoClass, field.getDeclaringClass()))
				throw new IllegalStateException("Cannot use If class " + ifClass.getName() + " on " + field
						+ " because the containing class " + field.getDeclaringClass().getName() + " is not compatible with " + pojoClass.getName());
		}

		return result;
	}

	/** */
	public If<?, ?> createIf(Class<? extends If<?, ?>> ifClass, Field field) {
		If<?, ?> created = fact.construct(ifClass);

		if (created instanceof InitializeIf)
			((InitializeIf)created).init(fact, field);

		return created;
	}
}


package com.googlecode.objectify.impl.translate;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.impl.LoadConditions;
import com.googlecode.objectify.impl.LoadEngine;
import com.googlecode.objectify.impl.Path;
import com.googlecode.objectify.repackaged.gentyref.GenericTypeReflector;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

/**
 * The context of a load operation, which may extend across several entities (for example, a batch).
 */
@Slf4j
public class LoadContext
{
	/** */
	LoadEngine engine;

	/** Lazily created, but executed at the end of done() */
	List<Runnable> deferred;

	/** The key of the current root entity; will change as multiple entities are loaded */
	Key<?> currentRoot;

	/** As we enter and exit embedded contexts, track the objects */
	Deque<Object> containers = new ArrayDeque<>();

	/**
	 * If a translator implements the marker interface Recycles, this will be populated with
	 * the existing value of a property.
	 */
	Object recycled;

	/** */
	public LoadContext(LoadEngine engine) {
		this.engine = engine;
	}

	/** The most recently recycled value. It can be used exactly once. */
	public Object useRecycled() {
		Object value = recycled;
		recycled = null;
		return value;
	}

	/** */
	public void recycle(Object value) {
		this.recycled = value;
	}

	/** Sets the current root entity */
	public void setCurrentRoot(Key<?> rootEntity) {
		this.currentRoot = rootEntity;
	}

	/**
	 * Call this when a load process completes.  Executes anything in the batch and then executes any delayed operations.
	 */
	public void done() {
		engine.execute();

		while (deferred != null) {
			final List<Runnable> runme = deferred;
			deferred = null;	// reset this because it might get filled with more

			for (final Runnable run: runme) {
				log.trace("Executing {}", run);
				run.run();
			}
		}
	}

	/**
	 * Create a Ref for the key, and maybe start a load operation depending on current load groups.
	 */
	public <T> Ref<T> loadRef(Key<T> key, LoadConditions loadConditions) {
		return engine.makeRef(currentRoot, loadConditions, key);
	}

	/**
	 * Delays an operation until the context is done().  Typically this is for lifecycle methods.
	 */
	public void defer(Runnable runnable) {
		if (this.deferred == null)
			this.deferred = new ArrayList<>();

		log.trace("Deferring: {}", runnable);

		this.deferred.add(runnable);
	}

	/**
	 * Get the container object which is appropriate for the specified property. Go up the chain looking for a compatible
	 * type; the first one found is the container. If nothing found, throw an exception.
	 */
	public Object getContainer(Type containerType, Path path) {
		Class<?> containerClass = GenericTypeReflector.erase(containerType);

		Iterator<Object> containersIt = containers.descendingIterator();

		// We have always entered the current 'this' context when processing properties, so the first thing
		// we get will always be 'this'. So skip that and the first matching owner should be what we want.
		containersIt.next();

		while (containersIt.hasNext()) {
			Object potentialContainer = containersIt.next();

			if (containerClass.isAssignableFrom(potentialContainer.getClass()))
				return potentialContainer;
		}

		throw new IllegalStateException("No container matching " + containerType + " in " + containers + " at path " + path);
	}

	/**
	 * Enter a container context; this is the context of the object that we are processing right now.
	 */
	public void enterContainerContext(Object container) {
		containers.addLast(container);
	}

	/**
	 * Exit a container context. The parameter is just a sanity check to make sure that the value popped off is the same
	 * as the value we expect.
	 */
	public void exitContainerContext(Object expectedContainer) {
		Object popped = containers.removeLast();
		assert popped == expectedContainer;
	}
}


package com.googlecode.objectify.impl;

import com.google.cloud.datastore.Key;
import com.googlecode.objectify.cache.MemcacheStats;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Tracks hit/miss statistics for the memcache.
 */
public class EntityMemcacheStats implements MemcacheStats
{
	/** */
	public class Stat
	{
		private AtomicLong hits = new AtomicLong();
		private AtomicLong misses = new AtomicLong();

		public long getHits() { return this.hits.get(); }
		public long getMisses() { return this.misses.get(); }

		public float getPercent()
		{
			long h = this.getHits();
			long m = this.getMisses();
			long total = h + m;

			if (total == 0)
				return 0;
			else
				return (float)h / (float)total;
		}
	}

	/** */
	private Map<String, Stat> stats = new ConcurrentHashMap<>();

	/**
	 * Get the live statistics.  You can clear it if you want.
	 *
	 * @return the live map, but you can iterate through it just fine
	 */
	public Map<String, Stat> getStats() { return this.stats; }

	/** */
	@Override
	public void recordHit(Key key)
	{
		this.getStat(key.getKind()).hits.incrementAndGet();
	}

	/** */
	@Override
	public void recordMiss(Key key)
	{
		this.getStat(key.getKind()).misses.incrementAndGet();
	}

	/**
	 * We're just tracking statistics so we don't really need to worry about these stepping on each other;
	 * if there's a hit or miss lost no big deal.
	 */
	private Stat getStat(String kind)
	{
		Stat stat = this.stats.get(kind);
		if (stat == null)
		{
			stat = new Stat();
			this.stats.put(kind, stat);
		}

		return stat;
	}
}


/*
 * $Id$
 */

package com.googlecode.objectify.util;

import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * This provides some of the methods of Google's (package-private) FutureHelper
 *
 * @see com.google.cloud.datastore.FutureHelper
 *
 * @author Jeff Schnitzer
 */
public class FutureHelper
{
	private FutureHelper() {
	}

	/** Quietly perform the get() on a future */
	public static <T> T quietGet(Future<T> future)
	{
		try
		{
			return future.get();
		}
		catch (Exception ex)
		{
			unwrapAndThrow(ex);
			return null;	// just to make the compiler happy
		}
	}

	/**
	 * Properly unwraps ExecutionException, throwing the relevant original cause.  Otherwise
	 * RuntimeExceptions get thrown and checked exceptions get wrapped in a RuntimeException.
	 */
	public static void unwrapAndThrow(Throwable ex)
	{
		if (ex instanceof RuntimeException)
			throw (RuntimeException)ex;
		else if (ex instanceof Error)
			throw (Error)ex;
		else if (ex instanceof ExecutionException)
			unwrapAndThrow(ex.getCause());
		else
			throw new UndeclaredThrowableException(ex);
	}
}


package com.googlecode.objectify.impl.translate;

import com.google.cloud.datastore.DoubleValue;
import com.google.cloud.datastore.Value;
import com.google.cloud.datastore.ValueType;
import com.google.common.primitives.Primitives;
import com.googlecode.objectify.impl.Path;

/**
 *
Floats are funky in the datastore.  You can save numbers of any size, but they always retrieve as Double.
 * For the hell of it, we also handle String in the datastore by trying to parse it.

 *
 *
Not a ValueTranslatorFactory because Numbers are not assignable to primitives.  Java lame.

 *
 * @author Jeff Schnitzer <jeff@infohazard.org>
 */
public class FloatTranslatorFactory implements TranslatorFactory<Number, Double>
{
	@Override
	public Translator<Number, Double> create(final TypeKey<Number> tk, final CreateContext ctx, final Path path) {
		final Class<?> clazz = Primitives.wrap(tk.getTypeAsClass());

		if (!(clazz == Float.class || clazz == Double.class))
			return null;

		return new ValueTranslator<Number, Double>(ValueType.DOUBLE, ValueType.STRING) {
			@Override
			protected Number loadValue(final Value<Double> value, final LoadContext ctx, final Path path) throws SkipException {
				if (value.getType() == ValueType.STRING) {
					try {
						return coerceNumber(Double.valueOf(((Value<String>)(Value)value).get()), clazz);
					} catch (NumberFormatException ex) {}
				}
				else {
					return coerceNumber(value.get(), clazz);
				}

				path.throwIllegalState("Don't know how to translate " + value + " to a number");
				return null;	// never gets here
			}

			@Override
			protected Value<Double> saveValue(final Number value, final SaveContext ctx, final Path path) throws SkipException {
				return DoubleValue.of(value.doubleValue());
			}
		};
	}

	/**
	 * Coerces the value to be a number of the specified type; needed because
	 * all numbers come back from the datastore as Long/Double and this screws up
	 * any type that expects something smaller. We don't need to worry about primitive
	 * types because we wrapped the class earlier.
	 */
	private Number coerceNumber(final Number value, final Class<?> type) {
		if (type == Float.class) return value.floatValue();
		else if (type == Double.class) return value.doubleValue();
		else throw new IllegalArgumentException();	// should be impossible
	}
}


package com.googlecode.objectify.impl.translate;

import com.google.cloud.datastore.FullEntity;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.impl.Path;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
Manages all the translators used to map between POJO fields and the
 * types that the Datastore can actually persist.  Essentially acts as an
 * aggregator for all the TranslatorFactory objects.

 *
 *
When Objectify arranges a translator for a type at registration time, it runs
 * through the available TranslatorFactory instances one at time looking for one that
 * will provide a Translator.  The first one found is kept and used during runtime
 * assembly and disassembly of entities.

 *
 * @author Jeff Schnitzer <jeff@infohazard.org>
 */
public class Translators
{
	/** */
	private final ObjectifyFactory fact;

	/** */
	private final List<TranslatorFactory<?, ?>> translatorFactories = new ArrayList<>();

	/** Where we should insert new translators */
	private int insertPoint;

	/** Where we should insert new early translators */
	private int earlyInsertPoint;

	/**
	 * This needs to be threadsafe because new translators can show up at runtime. Filtering
	 * and ObjectTranslatorFactory can both create new translators dynamically.
	 */
	private final Map<TypeKey, Translator<?, ?>> translators = new ConcurrentHashMap<>();

	/**
	 * Initialize the default set of converters in the proper order.
	 */
	public Translators(final ObjectifyFactory fact) {
		this.fact = fact;

		// The order is CRITICAL!
		this.translatorFactories.add(new TranslateTranslatorFactory(true));	// Early translators get first shot at everything

		// Magic inflection point at which we want to prioritize added EARLY translators
		this.earlyInsertPoint = this.translatorFactories.size();

		// Annotation based translators go first
		this.translatorFactories.add(new ContainerTranslatorFactory());
		this.translatorFactories.add(new SerializeTranslatorFactory());	// Serialize has priority over everything
		this.translatorFactories.add(new MapifyTranslatorFactory());

		// Magic inflection point at which we want to prioritize added normal translators
		this.insertPoint = this.translatorFactories.size();

		this.translatorFactories.add(new ByteArrayTranslatorFactory());
		this.translatorFactories.add(new ArrayTranslatorFactory());		// AFTER byte array otherwise we will occlude it
		this.translatorFactories.add(new CollectionTranslatorFactory());
		this.translatorFactories.add(new EmbeddedMapTranslatorFactory());
		this.translatorFactories.add(new TranslateTranslatorFactory(false));	// Late translators get a shot after collections

		this.translatorFactories.add(new StringTranslatorFactory());
		this.translatorFactories.add(new IntegerTranslatorFactory());
		this.translatorFactories.add(new FloatTranslatorFactory());
		this.translatorFactories.add(new BooleanTranslatorFactory());
		this.translatorFactories.add(new KeyTranslatorFactory());
		this.translatorFactories.add(new RawKeyTranslatorFactory());
		this.translatorFactories.add(new RawEntityTranslatorFactory());
		this.translatorFactories.add(new TimestampTranslatorFactory());
		this.translatorFactories.add(new RefTranslatorFactory());
		this.translatorFactories.add(new EnumTranslatorFactory());
		this.translatorFactories.add(new SqlDateTranslatorFactory());
		this.translatorFactories.add(new DateTranslatorFactory());
		this.translatorFactories.add(new TimeZoneTranslatorFactory());
		this.translatorFactories.add(new URLTranslatorFactory());
		this.translatorFactories.add(new LatLngTranslatorFactory());
		this.translatorFactories.add(new BlobTranslatorFactory());
		this.translatorFactories.add(new RawValueTranslatorFactory());
		this.translatorFactories.add(new ObjectTranslatorFactory(this));

		// LAST! It catches everything.
		this.translatorFactories.add(new ClassTranslatorFactory<>());
	}

	/**
	 *
Add a new translator to the list.  Translators are added in order after most of the "plumbing" translators
	 * (collections, arrays, maps, serialize, embeds, references) but before any of the standard value conversions
	 * like String, Number, Key, Enum, SqlDate, TimeZone, etc.

	 *
	 *
Translators are added in-order so earlier translaters pre-empt later translators.

	 */
	public void add(final TranslatorFactory<?, ?> trans) {
		this.translatorFactories.add(insertPoint, trans);
		insertPoint++;
	}

	/**
	 *
Add a new translator to the beginning of the list, before all other translators
	 * except other translators that have been added early.

	 */
	public void addEarly(final TranslatorFactory<?, ?> trans) {
		this.translatorFactories.add(earlyInsertPoint, trans);
		earlyInsertPoint++;
		insertPoint++;
	}

	/**
	 * Obtains the Translator appropriate for this type and annotations. May be a cached
	 * translator; if not, one will be discovered and cached.
	 */
	public
 Translator
 get(final TypeKey tk, final CreateContext ctx, final Path path) {

		Translator<?, ?> translator = translators.get(tk);
		if (translator == null) {
			translator = create(tk, ctx, path);
			translators.put(tk, translator);
		}

		//noinspection unchecked
		return (Translator
)translator;
	}

	/**
	 * Get the translator for a root entity class
	 */
	public
 Translator<P, FullEntity<?>> getRoot(final Class
 clazz) {
		return get(new TypeKey(clazz), new CreateContext(fact), Path.root());
	}

	/**
	 * Create a translator from scratch by going through the discovery process.
	 */
	private Translator<?, ?> create(final TypeKey tk, final CreateContext ctx, final Path path) {
		for (final TranslatorFactory<?, ?> trans: this.translatorFactories) {
			@SuppressWarnings("unchecked")
			final Translator<?, ?> soFar = trans.create(tk, ctx, path);
			if (soFar != null)
				return soFar;
		}

		throw new IllegalArgumentException("Don't know how to translate " + tk.getType() + " with annotations " + Arrays.toString(tk.getAnnotations()));
	}
}


package com.googlecode.objectify.impl;

import com.google.cloud.datastore.FullEntity;
import com.google.cloud.datastore.Value;
import com.googlecode.objectify.annotation.Load;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.impl.translate.LoadContext;
import com.googlecode.objectify.impl.translate.Populator;
import com.googlecode.objectify.impl.translate.Recycles;
import com.googlecode.objectify.impl.translate.SaveContext;
import com.googlecode.objectify.impl.translate.SkipException;
import com.googlecode.objectify.impl.translate.Synthetic;
import com.googlecode.objectify.impl.translate.Translator;
import com.googlecode.objectify.util.LogUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * Associates a Property with a Translator and provides a more convenient interface.
 */
@RequiredArgsConstructor
@Slf4j
public class PropertyPopulator<P, D> implements Populator<P> {
	/** */
	@Getter
	private final Property property;

	private final Translator<P, D> translator;

	/** */
	public LoadConditions getLoadConditions() {
		return new LoadConditions(property.getAnnotation(Load.class), property.getAnnotation(Parent.class));
	}

	/** This is easier to debug if we have a string value */
	@Override
	public String toString() {
		return this.getClass().getSimpleName() + "(" + property.getName() + ")";
	}

	/**
	 * Gets the appropriate value from the container and sets it on the appropriate field of the pojo.
	 */
	@Override
	public void load(final FullEntity<?> container, final LoadContext ctx, final Path containerPath, final P intoPojo) {
		try {
			if (translator instanceof Recycles)
				ctx.recycle(property.get(intoPojo));

			final Value<D> value = (translator instanceof Synthetic)
				? null
				: getPropertyFromContainer(container, containerPath);	// will throw SkipException if property not present

			setValue(intoPojo, value, ctx, containerPath);
		}
		catch (SkipException ex) {
			// Irrelevant
		}
	}

	/**
	 * Gets the relevant property from the container, detecting alsoload collisions.
	 *
	 * @return the value obtained from the container
	 * @throws IllegalStateException if there are multiple alsoload name matches
	 */
	private Value<D> getPropertyFromContainer(final FullEntity<?> container, final Path containerPath) {
		String foundName = null;
		Value<D> value = null;

		for (String name: property.getLoadNames()) {
			if (container.contains(name)) {
				if (foundName != null)
					throw new IllegalStateException("Collision trying to load field; multiple name matches for '"
							+ property.getName() + "' at '" + containerPath.extend(foundName) + "' and '" + containerPath.extend(name) + "'");

				//noinspection unchecked
				value = container.getValue(name);
				foundName = name;
			}
		}

		if (foundName == null)
			throw new SkipException();
		else
			return value;
	}

	/**
	 * Set this raw datastore value on the relevant property of the pojo, doing whatever translations are necessary.
	 */
	public void setValue(final Object pojo, final Value<D> value, final LoadContext ctx, final Path containerPath) throws SkipException {
		final Path propertyPath = containerPath.extend(property.getName());
		final P loaded = translator.load(value, ctx, propertyPath);

		setOnPojo(pojo, loaded, ctx, propertyPath);
	}

	/**
	 * Sets the property on the pojo to the value. The value should already be translated.
	 * TODO: Sensitive to the value possibly being a Result<?> wrapper, in which case it enqueues the set operation until the loadcontext is done.
	 */
	private void setOnPojo(final Object pojo, final P value, final LoadContext ctx, final Path path) {
		if (log.isTraceEnabled())
			log.trace(LogUtils.msg(path, "Setting property " + property.getName() + " to " + value));

		property.set(pojo, value);
	}

	/**
	 * Gets the appropriate field value from the pojo and puts it in the container at the appropriate prop name
	 * and with the appropriate indexing.
	 * @param onPojo is the parent pojo which holds the property we represent
	 * @param index is the default state of indexing up to this point
	 * @param containerPath is the path to the container; each property will extend this path.
	 */
	@Override
	public void save(final P onPojo, boolean index, final SaveContext ctx, final Path containerPath, final FullEntity.Builder<?> into) {
		if (property.isSaved(onPojo)) {
			// Look for an override on indexing
			final Boolean propertyIndexInstruction = property.getIndexInstruction(onPojo);
			if (propertyIndexInstruction != null)
				index = propertyIndexInstruction;

			@SuppressWarnings("unchecked")
			final P value = (P)property.get(onPojo);
			try {
				final Path propPath = containerPath.extend(property.getName());
				final Value<D> propValue = translator.save(value, index, ctx, propPath);

				into.set(property.getName(), propValue);
			}
			catch (SkipException ex) {
				// No problem, do nothing
			}
		}
	}

	/**
	 * Get the value for the property and translate it into datastore format.
	 */
	public Value<D> getValue(final Object pojo, final SaveContext ctx, final Path containerPath) {
		@SuppressWarnings("unchecked")
		final P value = (P)property.get(pojo);

		return translator.save(value, false, ctx, containerPath.extend(property.getName()));
	}

}


/*
 * 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 com.fasterxml.jackson.core.JsonGenerator;
import com.google.openrtb.util.OpenRtbUtils;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.GeneratedMessageV3.ExtendableMessage;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * Serializes OpenRTB messages to JSON.
 */
public class AbstractOpenRtbJsonWriter {
  private final OpenRtbJsonFactory factory;

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

  public final OpenRtbJsonFactory factory() {
    return factory;
  }

  /**
   * Write any extensions that may exist in a message.
   *
   * @param msg A message that may contain extensions
   * @param gen The JSON generator
   * @param <EM> Type of message being serialized, containing extensions
   * @throws IOException any serialization error
   */
  @SuppressWarnings("unchecked")
  protected final <EM extends ExtendableMessage<EM>>
  void writeExtensions(EM msg, JsonGenerator gen) throws IOException {
    boolean openExt = false;

    for (Map.Entry<FieldDescriptor, Object> field : msg.getAllFields().entrySet()) {
      FieldDescriptor fd = field.getKey();
      if (fd.isExtension()) {
        if (fd.isRepeated()) {
          List<Object> extValue = (List<Object>) field.getValue();
          if (!extValue.isEmpty()) {
            OpenRtbJsonExtWriter<Object> extWriter =
                factory.getWriter(msg.getClass(), extValue.get(0).getClass(), fd.getName());
            if (extWriter != null) {
              openExt = openExt(gen, openExt);
              extWriter.writeRepeated(extValue, gen);
            }
          }
        } else {
          Object extValue = field.getValue();
          OpenRtbJsonExtWriter<Object> extWriter =
              factory.getWriter(msg.getClass(), extValue.getClass(), fd.getName());
          if (extWriter != null) {
            openExt = openExt(gen, openExt);
            extWriter.writeSingle(extValue, gen);
          }
        }
      }
    }

    if (openExt) {
      gen.writeEndObject();
    }
  }

  private static boolean openExt(JsonGenerator gen, boolean openExt) throws IOException {
    if (!openExt) {
      gen.writeObjectFieldStart("ext");
    }
    return true;
  }

  protected final boolean checkRequired(boolean hasProperty) {
    if (!hasProperty) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Required property not set");
      } else {
        return false;
      }
    }
    return true;
  }

  protected final boolean checkRequired(int propertyCount) {
    if (propertyCount == 0) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Required property is empty");
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * Writes a string that represents a ContentCategory's JSON name, returning success status.
   * If the factory is in strict mode, the category name is validated.
   */
  protected final boolean writeContentCategory(String cat, JsonGenerator gen) throws IOException {
    if (!factory.isStrict() || OpenRtbUtils.categoryFromName(cat) != null) {
      gen.writeString(cat);
      return true;
    } else {
      return false;
    }
  }

  /**
   * Writes an array of ContentCategory if not empty.
   *
   * @see #writeContentCategory(String, JsonGenerator)
   */
  protected final void writeContentCategories(
      String fieldName, List<String> cats, JsonGenerator gen)
      throws IOException {
    if (!cats.isEmpty()) {
      gen.writeArrayFieldStart(fieldName);
      for (String cat : cats) {
        writeContentCategory(cat, gen);
      }
      gen.writeEndArray();
    }
  }
}


/*
 * 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 static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.escape.Escaper;
import com.google.common.net.PercentEscaper;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid.Bid;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Supports preprocessing for "snippets" of textual information, in particular for
 * various String fields from the response {@link Bid}s such as the ad markup, URLs and IDs.
 * Additionally, you can use the syntax %{...}% for URL encoding. Nesting can be used, e.g.
 * %{A%{B}%}% will encode A and doubly-encode B. This nesting is typically necessary when URLs
 * have parameter that contain other URLs, so each server decodes and redirects to the next URL.
 *
 * <p>This class is threadsafe, and all concrete subclasses have to be too.
 */
public abstract class SnippetProcessor {
  private static final Logger logger = LoggerFactory.getLogger(SnippetProcessor.class);
  private static final Escaper ESCAPER = new PercentEscaper("-_.*", true);

  public static final SnippetProcessor NULL = new SnippetProcessor() {
    @Override public String process(SnippetProcessorContext ctx, String snippet) {
      checkNotNull(ctx);
      return checkNotNull(snippet);
    }

    @Override protected boolean processMacroAt(
        SnippetProcessorContext ctx, SnippetMacroType macroDef) {
      return false;
    }
  };

  private final ImmutableList<SnippetMacroType> scanMacros;

  public SnippetProcessor() {
    List<SnippetMacroType> registered = registerMacros();
    SnippetMacroType[] macros = registered.toArray(new SnippetMacroType[registered.size()]);
    Arrays.sort(macros, new Comparator<SnippetMacroType>() {
      @Override public int compare(SnippetMacroType o1, SnippetMacroType o2) {
        return o1.key().compareTo(o2.key());
      }});
    this.scanMacros = ImmutableList.copyOf(macros);
  }

  protected List<SnippetMacroType> registerMacros() {
    return ImmutableList.of();
  }

  public static Escaper getEscaper() {
    return ESCAPER;
  }

  /**
   * Processes the raw snippet that was set by the bid, making any transformations necessary.
   */
  public String process(SnippetProcessorContext ctx, String snippet) {
    checkNotNull(ctx);
    StringBuilder sb = ctx.builder();
    sb.setLength(0);
    String currSnippet = snippet;

    boolean processedMacros = false;
    int snippetPos = 0;
    int macroPos = currSnippet.indexOf("${");

    while (macroPos != -1) {
      sb.append(currSnippet.substring(snippetPos, macroPos));
      int macroEnd = processMacroAt(ctx, currSnippet, macroPos);

      if (macroEnd == -1) {
        sb.append("${");
        snippetPos = macroPos + 2;
      } else {
        snippetPos = macroEnd;
        processedMacros = true;
      }

      macroPos = currSnippet.indexOf("${", snippetPos);
    }

    if (processedMacros) {
      sb.append(currSnippet, snippetPos, currSnippet.length());
      currSnippet = sb.toString();
    }
    sb.setLength(0);

    String ret = urlEncode(ctx, currSnippet);
    sb.setLength(0);
    return ret;
  }

  private int processMacroAt(SnippetProcessorContext ctx,
      String snippet, int macroStart) {
    SnippetMacroType macroDef = match(snippet, macroStart);
    if (macroDef == null) {
      return -1;
    }

    processMacroAt(ctx, macroDef);

    // Handle recursive macros
    StringBuilder sb = ctx.builder();
    int macroPos = sb.indexOf("${");
    if (macroPos != -1) {
      String recSnippet = sb.substring(macroPos);
      // Avoid infinite recursion if the macro expands to itself!
      if (!macroDef.key().equals(recSnippet)) {
        String recReplaced = process(ctx.rec(), recSnippet);
        if (recReplaced != recSnippet) {
          sb.setLength(macroPos);
          sb.append(recReplaced);
        }
      }
    }

    return macroStart + macroDef.key().length();
  }

  private SnippetMacroType match(String snippet, int macroStart) {
    for (SnippetMacroType macroDef : scanMacros) {
      if (macroDef.key().regionMatches(0, snippet, macroStart, macroDef.key().length())) {
        return macroDef;
      }
    }
    return null;
  }

  protected abstract boolean processMacroAt(
      SnippetProcessorContext ctx, SnippetMacroType macroDef);

  protected static String urlEncode(SnippetProcessorContext ctx, String snippet) {
    int snippetPos = snippet.indexOf("%{");
    if (snippetPos == -1) {
      return snippet;
    }

    StringBuilder sb = ctx.builder();
    int encodeLevel = 0;
    int encodeStart = 0;
    int lastPos = snippet.length() - 1;

    while (snippetPos <= lastPos) {
      char c = snippet.charAt(snippetPos);

      if (c == '%' && snippetPos < lastPos && snippet.charAt(snippetPos + 1) == '{') {
        sb.append(flushEncoding(snippet, encodeStart, snippetPos, encodeLevel++));
        encodeStart = (snippetPos += 2);
      } else if (c == '}' && snippetPos < lastPos && snippet.charAt(snippetPos + 1) == '%'
          && encodeLevel > 0) {
        sb.append(flushEncoding(snippet, encodeStart, snippetPos, encodeLevel--));
        encodeStart = (snippetPos += 2);
      } else {
        ++snippetPos;
      }
    }

    if (encodeLevel != 0) {
      logger.warn("Unbalanced '%{': level={}, pos={}", encodeLevel, snippetPos);
    }

    return sb.append(flushEncoding(snippet, encodeStart, snippet.length(), 0)).toString();
  }

  private static String flushEncoding(
      String snippet, int encodeStart, int encodeEnd, int encodeLevel) {
    String substr = snippet.substring(encodeStart, encodeEnd);

    for (int i = 0; i < encodeLevel; ++i) {
      substr = getEscaper().escape(substr);
    }

    return substr;
  }

  @Override public final String toString() {
    return toStringHelper().toString();
  }

  protected ToStringHelper toStringHelper() {
    return MoreObjects.toStringHelper(this).omitNullValues();
  }
}


/*
 * 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.Gender;
import com.google.openrtb.OpenRtb.BidRequest;
import com.google.openrtb.OpenRtb.BidRequest.App;
import com.google.openrtb.OpenRtb.BidRequest.Content;
import com.google.openrtb.OpenRtb.BidRequest.Data;
import com.google.openrtb.OpenRtb.BidRequest.Data.Segment;
import com.google.openrtb.OpenRtb.BidRequest.Device;
import com.google.openrtb.OpenRtb.BidRequest.Geo;
import com.google.openrtb.OpenRtb.BidRequest.Imp;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Audio;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Banner;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Banner.Format;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Metric;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Native;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Pmp;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Pmp.Deal;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Video;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Video.CompanionAd;
import com.google.openrtb.OpenRtb.BidRequest.Producer;
import com.google.openrtb.OpenRtb.BidRequest.Publisher;
import com.google.openrtb.OpenRtb.BidRequest.Regs;
import com.google.openrtb.OpenRtb.BidRequest.Site;
import com.google.openrtb.OpenRtb.BidRequest.Source;
import com.google.openrtb.OpenRtb.BidRequest.User;
import com.google.openrtb.OpenRtb.BidResponse;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid.Bid;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Serializes OpenRTB {@link BidRequest}/{@link BidResponse} 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 OpenRtbJsonWriter extends AbstractOpenRtbJsonWriter {
  private OpenRtbNativeJsonWriter nativeWriter;

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

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

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

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

  /**
   * Serializes a {@link BidRequest} to JSON, with a provided {@link JsonGenerator}
   * which allows several choices of output and encoding.
   */
  public final void writeBidRequest(BidRequest req, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeBidRequestFields(req, gen);
    writeExtensions(req, gen);
    gen.writeEndObject();
    gen.flush();
  }

  protected void writeBidRequestFields(BidRequest req, JsonGenerator gen) throws IOException {
    gen.writeStringField("id", req.getId());
    if (checkRequired(req.getImpCount())) {
      gen.writeArrayFieldStart("imp");
      for (Imp imp : req.getImpList()) {
        writeImp(imp, gen);
      }
      gen.writeEndArray();
    }
    switch (req.getDistributionchannelOneofCase()) {
      case SITE:
        gen.writeFieldName("site");
        writeSite(req.getSite(), gen);
        break;
      case APP:
        gen.writeFieldName("app");
        writeApp(req.getApp(), gen);
        break;
      case DISTRIBUTIONCHANNELONEOF_NOT_SET:
        checkRequired(false);
    }
    if (req.hasDevice()) {
      gen.writeFieldName("device");
      writeDevice(req.getDevice(), gen);
    }
    if (req.hasUser()) {
      gen.writeFieldName("user");
      writeUser(req.getUser(), gen);
    }
    if (req.hasTest()) {
      writeIntBoolField("test", req.getTest(), gen);
    }
    if (req.hasAt()) {
      writeEnumField("at", req.getAt(), gen);
    }
    if (req.hasTmax()) {
      gen.writeNumberField("tmax", req.getTmax());
    }
    writeStrings("wseat", req.getWseatList(), gen);
    if (req.hasAllimps()) {
      writeIntBoolField("allimps", req.getAllimps(), gen);
    }
    writeStrings("cur", req.getCurList(), gen);
    writeContentCategories("bcat", req.getBcatList(), gen);
    writeStrings("badv", req.getBadvList(), gen);
    if (req.hasRegs()) {
      gen.writeFieldName("regs");
      writeRegs(req.getRegs(), gen);
    }
    writeStrings("bapp", req.getBappList(), gen);
    writeStrings("bseat", req.getBseatList(), gen);
    writeStrings("wlang", req.getWlangList(), gen);
    if (req.hasSource()) {
      gen.writeFieldName("source");
      writeSource(req.getSource(), gen);
    }
  }

  public final void writeImp(Imp imp, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeImpFields(imp, gen);
    writeExtensions(imp, gen);
    gen.writeEndObject();
  }

  protected void writeImpFields(Imp imp, JsonGenerator gen) throws IOException {
    gen.writeStringField("id", imp.getId());
    if (imp.hasBanner()) {
      gen.writeFieldName("banner");
      writeBanner(imp.getBanner(), gen);
    }
    if (imp.hasVideo()) {
      gen.writeFieldName("video");
      writeVideo(imp.getVideo(), gen);
    }
    if (imp.hasAudio()) {
      gen.writeFieldName("audio");
      writeAudio(imp.getAudio(), gen);
    }
    if (imp.hasNative()) {
      gen.writeFieldName("native");
      writeNative(imp.getNative(), gen);
    }
    if (imp.hasDisplaymanager()) {
      gen.writeStringField("displaymanager", imp.getDisplaymanager());
    }
    if (imp.hasDisplaymanagerver()) {
      gen.writeStringField("displaymanagerver", imp.getDisplaymanagerver());
    }
    if (imp.hasInstl()) {
      writeIntBoolField("instl", imp.getInstl(), gen);
    }
    if (imp.hasTagid()) {
      gen.writeStringField("tagid", imp.getTagid());
    }
    if (imp.hasBidfloor()) {
      gen.writeNumberField("bidfloor", imp.getBidfloor());
    }
    if (imp.hasBidfloorcur()) {
      gen.writeStringField("bidfloorcur", imp.getBidfloorcur());
    }
    if (imp.hasSecure()) {
      writeIntBoolField("secure", imp.getSecure(), gen);
    }
    writeStrings("iframebuster", imp.getIframebusterList(), gen);
    if (imp.hasPmp()) {
      gen.writeFieldName("pmp");
      writePmp(imp.getPmp(), gen);
    }
    if (imp.hasClickbrowser()) {
      gen.writeBooleanField("clickbrowser", imp.getClickbrowser());
    }
    if (imp.hasExp()) {
      gen.writeNumberField("exp", imp.getExp());
    }
    if (imp.getMetricCount() != 0) {
      gen.writeArrayFieldStart("metric");
      for (Metric metric : imp.getMetricList()) {
        writeMetric(metric, gen);
      }
      gen.writeEndArray();
    }
  }

  public final void writeMetric(Metric metric, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeMetricFields(metric, gen);
    writeExtensions(metric, gen);
    gen.writeEndObject();
  }

  protected void writeMetricFields(Metric metric, JsonGenerator gen) throws IOException {
    if (metric.hasType()) {
      gen.writeStringField("type", metric.getType());
    }
    if (metric.hasValue()) {
      gen.writeNumberField("value", metric.getValue());
    }
    if (metric.hasVendor()) {
      gen.writeStringField("vendor", metric.getVendor());
    }
  }

  public final void writeBanner(Banner banner, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeBannerFields(banner, gen);
    writeExtensions(banner, gen);
    gen.writeEndObject();
  }

  @SuppressWarnings("deprecation")
  protected void writeBannerFields(Banner banner, JsonGenerator gen) throws IOException {
    if (banner.hasW()) {
      gen.writeNumberField("w", banner.getW());
    }
    if (banner.hasH()) {
      gen.writeNumberField("h", banner.getH());
    }
    if (banner.hasWmax()) {
      gen.writeNumberField("wmax", banner.getWmax());
    }
    if (banner.hasHmax()) {
      gen.writeNumberField("hmax", banner.getHmax());
    }
    if (banner.hasWmin()) {
      gen.writeNumberField("wmin", banner.getWmin());
    }
    if (banner.hasHmin()) {
      gen.writeNumberField("hmin", banner.getHmin());
    }
    if (banner.hasId()) {
      gen.writeStringField("id", banner.getId());
    }
    writeEnums("btype", banner.getBtypeList(), gen);
    writeEnums("battr", banner.getBattrList(), gen);
    if (banner.hasPos()) {
      writeEnumField("pos", banner.getPos(), gen);
    }
    writeStrings("mimes", banner.getMimesList(), gen);
    if (banner.hasTopframe()) {
      writeIntBoolField("topframe", banner.getTopframe(), gen);
    }
    writeEnums("expdir", banner.getExpdirList(), gen);
    writeEnums("api", banner.getApiList(), gen);
    if (checkRequired(banner.getFormatCount())) {
      gen.writeArrayFieldStart("format");
      for (Format format : banner.getFormatList()) {
        writeFormat(format, gen);
      }
      gen.writeEndArray();
    }
    if (banner.hasVcm()) {
      writeIntBoolField("vcm", banner.getVcm(), gen);
    }
  }

  public final void writeFormat(Format format, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeFormatFields(format, gen);
    writeExtensions(format, gen);
    gen.writeEndObject();
  }

  protected void writeFormatFields(Format format, JsonGenerator gen) throws IOException {
    if (format.hasW()) {
      gen.writeNumberField("w", format.getW());
    }
    if (format.hasH()) {
      gen.writeNumberField("h", format.getH());
    }
    if (format.hasWratio()) {
      gen.writeNumberField("wratio", format.getWratio());
    }
    if (format.hasHratio()) {
      gen.writeNumberField("hratio", format.getHratio());
    }
    if (format.hasWmin()) {
      gen.writeNumberField("wmin", format.getWmin());
    }
  }

  public final void writeVideo(Video video, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeVideoFields(video, gen);
    writeExtensions(video, gen);
    gen.writeEndObject();
  }

  @SuppressWarnings("deprecation")
  protected void writeVideoFields(Video video, JsonGenerator gen) throws IOException {
    if (checkRequired(video.getMimesCount())) {
      writeStrings("mimes", video.getMimesList(), gen);
    }
    if (video.hasMinduration()) {
      gen.writeNumberField("minduration", video.getMinduration());
    }
    if (video.hasMaxduration()) {
      gen.writeNumberField("maxduration", video.getMaxduration());
    }
    if (video.hasProtocol()) {
      writeEnumField("protocol", video.getProtocol(), gen);
    }
    if (checkRequired(video.getProtocolsCount())) {
      writeEnums("protocols", video.getProtocolsList(), gen);
    }
    if (video.hasW()) {
      gen.writeNumberField("w", video.getW());
    }
    if (video.hasH()) {
      gen.writeNumberField("h", video.getH());
    }
    if (video.hasStartdelay()) {
      gen.writeNumberField("startdelay", video.getStartdelay());
    }
    if (video.hasLinearity()) {
      writeEnumField("linearity", video.getLinearity(), gen);
    }
    if (video.hasSequence()) {
      gen.writeNumberField("sequence", video.getSequence());
    }
    writeEnums("battr", video.getBattrList(), gen);
    if (video.hasMaxextended()) {
      gen.writeNumberField("maxextended", video.getMaxextended());
    }
    if (video.hasMinbitrate()) {
      gen.writeNumberField("minbitrate", video.getMinbitrate());
    }
    if (video.hasMaxbitrate()) {
      gen.writeNumberField("maxbitrate", video.getMaxbitrate());
    }
    if (video.hasBoxingallowed()) {
      writeIntBoolField("boxingallowed", video.getBoxingallowed(), gen);
    }
    writeEnums("playbackmethod", video.getPlaybackmethodList(), gen);
    writeEnums("delivery", video.getDeliveryList(), gen);
    if (video.hasPos()) {
      writeEnumField("pos", video.getPos(), gen);
    }
    if (video.getCompanionadCount() != 0) {
      // OpenRTB 2.2+
      gen.writeArrayFieldStart("companionad");
      for (Banner companionad : video.getCompanionadList()) {
        writeBanner(companionad, gen);
      }
      gen.writeEndArray();
    }
    if (video.hasCompanionad21()) {
      // OpenRTB 2.1-
      gen.writeFieldName("companionad");
      writeCompanionAd21(video.getCompanionad21(), gen);
    }
    writeEnums("api", video.getApiList(), gen);
    writeEnums("companiontype", video.getCompaniontypeList(), gen);
    if (video.hasSkip()) {
      writeIntBoolField("skip", video.getSkip(), gen);
    }
    if (video.hasSkipmin()) {
      gen.writeNumberField("skipmin", video.getSkipmin());
    }
    if (video.hasSkipafter()) {
      gen.writeNumberField("skipafter", video.getSkipafter());
    }
    if (video.hasPlacement()) {
      writeEnumField("placement", video.getPlacement(), gen);
    }
    if (video.hasPlaybackend()) {
      writeEnumField("playbackend", video.getPlaybackend(), gen);
    }
  }

  public final void writeCompanionAd21(CompanionAd companionad21, JsonGenerator gen)
      throws IOException {
    gen.writeStartObject();
    writeCompanionAd21Fields(companionad21, gen);
    writeExtensions(companionad21, gen);
    gen.writeEndObject();
  }

  protected void writeCompanionAd21Fields(CompanionAd companionad21, JsonGenerator gen)
      throws IOException {
    if (companionad21.getBannerCount() != 0) {
      gen.writeArrayFieldStart("banner");
      for (Banner banner : companionad21.getBannerList()) {
        writeBanner(banner, gen);
      }
      gen.writeEndArray();
    }
  }

  public final void writeAudio(Audio audio, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeAudioFields(audio, gen);
    writeExtensions(audio, gen);
    gen.writeEndObject();
  }

  protected void writeAudioFields(Audio audio, JsonGenerator gen) throws IOException {
    // Common to Video & Audio

    if (checkRequired(audio.getMimesCount())) {
      writeStrings("mimes", audio.getMimesList(), gen);
    }
    if (audio.hasMinduration()) {
      gen.writeNumberField("minduration", audio.getMinduration());
    }
    if (audio.hasMaxduration()) {
      gen.writeNumberField("maxduration", audio.getMaxduration());
    }
    if (checkRequired(audio.getProtocolsCount())) {
      writeEnums("protocols", audio.getProtocolsList(), gen);
    }
    if (audio.hasStartdelay()) {
      gen.writeNumberField("startdelay", audio.getStartdelay());
    }
    if (audio.hasSequence()) {
      gen.writeNumberField("sequence", audio.getSequence());
    }
    writeEnums("battr", audio.getBattrList(), gen);
    if (audio.hasMaxextended()) {
      gen.writeNumberField("maxextended", audio.getMaxextended());
    }
    if (audio.hasMinbitrate()) {
      gen.writeNumberField("minbitrate", audio.getMinbitrate());
    }
    if (audio.hasMaxbitrate()) {
      gen.writeNumberField("maxbitrate", audio.getMaxbitrate());
    }
    writeEnums("delivery", audio.getDeliveryList(), gen);
    if (audio.getCompanionadCount() != 0) {
      // OpenRTB 2.2+
      gen.writeArrayFieldStart("companionad");
      for (Banner companionad : audio.getCompanionadList()) {
        writeBanner(companionad, gen);
      }
      gen.writeEndArray();
    }
    writeEnums("api", audio.getApiList(), gen);
    writeEnums("companiontype", audio.getCompaniontypeList(), gen);

    // Audio only

    if (audio.hasMaxseq()) {
      gen.writeNumberField("maxseq", audio.getMaxseq());
    }
    if (audio.hasFeed()) {
      writeEnumField("feed", audio.getFeed(), gen);
    }
    if (audio.hasStitched()) {
      writeIntBoolField("stitched", audio.getStitched(), gen);
    }
    if (audio.hasNvol()) {
      writeEnumField("nvol", audio.getNvol(), gen);
    }
  }

  public final void writeNative(Native nativ, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeNativeFields(nativ, gen);
    writeExtensions(nativ, gen);
    gen.writeEndObject();
  }

  protected void writeNativeFields(Native nativ, JsonGenerator gen) throws IOException {
    switch (nativ.getRequestOneofCase()) {
      case REQUEST_NATIVE:
        gen.writeFieldName("request");
        if (factory().isForceNativeAsObject()) {
          nativeWriter().writeNativeRequest(nativ.getRequestNative(), gen);
        } else {
          gen.writeString(nativeWriter().writeNativeRequest(nativ.getRequestNative()));
        }
        break;
      case REQUEST:
        gen.writeStringField("request", nativ.getRequest());
        break;
      case REQUESTONEOF_NOT_SET:
        checkRequired(false);
    }
    if (nativ.hasVer()) {
      gen.writeStringField("ver", nativ.getVer());
    }
    writeEnums("api", nativ.getApiList(), gen);
    writeEnums("battr", nativ.getBattrList(), gen);
  }

  public final void writePmp(Pmp pmp, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writePmpFields(pmp, gen);
    writeExtensions(pmp, gen);
    gen.writeEndObject();
  }

  protected void writePmpFields(Pmp pmp, JsonGenerator gen) throws IOException {
    if (pmp.hasPrivateAuction()) {
      writeIntBoolField("private_auction", pmp.getPrivateAuction(), gen);
    }
    if (pmp.getDealsCount() != 0) {
      gen.writeArrayFieldStart("deals");
      for (Deal deals : pmp.getDealsList()) {
        writeDeal(deals, gen);
      }
      gen.writeEndArray();
    }
  }

  public final void writeDeal(Deal deal, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeDealFields(deal, gen);
    writeExtensions(deal, gen);
    gen.writeEndObject();
  }

  protected void writeDealFields(Deal deal, JsonGenerator gen) throws IOException {
    gen.writeStringField("id", deal.getId());
    if (deal.hasBidfloor()) {
      gen.writeNumberField("bidfloor", deal.getBidfloor());
    }
    if (deal.hasBidfloorcur()) {
      gen.writeStringField("bidfloorcur", deal.getBidfloorcur());
    }
    writeStrings("wseat", deal.getWseatList(), gen);
    writeStrings("wadomain", deal.getWadomainList(), gen);
    if (deal.hasAt()) {
      writeEnumField("at", deal.getAt(), gen);
    }
  }

  public final void writeSite(Site site, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeSiteFields(site, gen);
    writeExtensions(site, gen);
    gen.writeEndObject();
  }

  protected void writeSiteFields(Site site, JsonGenerator gen) throws IOException {
    if (site.hasId()) {
      gen.writeStringField("id", site.getId());
    }
    if (site.hasName()) {
      gen.writeStringField("name", site.getName());
    }
    if (site.hasDomain()) {
      gen.writeStringField("domain", site.getDomain());
    }
    writeContentCategories("cat", site.getCatList(), gen);
    writeContentCategories("sectioncat", site.getSectioncatList(), gen);
    writeContentCategories("pagecat", site.getPagecatList(), gen);
    if (site.hasPage()) {
      gen.writeStringField("page", site.getPage());
    }
    if (site.hasRef()) {
      gen.writeStringField("ref", site.getRef());
    }
    if (site.hasSearch()) {
      gen.writeStringField("search", site.getSearch());
    }
    if (site.hasMobile()) {
      writeIntBoolField("mobile", site.getMobile(), gen);
    }
    if (site.hasPrivacypolicy()) {
      writeIntBoolField("privacypolicy", site.getPrivacypolicy(), gen);
    }
    if (site.hasPublisher()) {
      gen.writeFieldName("publisher");
      writePublisher(site.getPublisher(), gen);
    }
    if (site.hasContent()) {
      gen.writeFieldName("content");
      writeContent(site.getContent(), gen);
    }
    if (site.hasKeywords()) {
      gen.writeStringField("keywords", site.getKeywords());
    }
  }

  public final void writeApp(App app, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeAppFields(app, gen);
    writeExtensions(app, gen);
    gen.writeEndObject();
  }

  protected void writeAppFields(App app, JsonGenerator gen) throws IOException {
    if (app.hasId()) {
      gen.writeStringField("id", app.getId());
    }
    if (app.hasName()) {
      gen.writeStringField("name", app.getName());
    }
    if (app.hasBundle()) {
      gen.writeStringField("bundle", app.getBundle());
    }
    if (app.hasDomain()) {
      gen.writeStringField("domain", app.getDomain());
    }
    if (app.hasStoreurl()) {
      gen.writeStringField("storeurl", app.getStoreurl());
    }
    writeContentCategories("cat", app.getCatList(), gen);
    writeContentCategories("sectioncat", app.getSectioncatList(), gen);
    writeContentCategories("pagecat", app.getPagecatList(), gen);
    if (app.hasVer()) {
      gen.writeStringField("ver", app.getVer());
    }
    if (app.hasPrivacypolicy()) {
      writeIntBoolField("privacypolicy", app.getPrivacypolicy(), gen);
    }
    if (app.hasPaid()) {
      writeIntBoolField("paid", app.getPaid(), gen);
    }
    if (app.hasPublisher()) {
      gen.writeFieldName("publisher");
      writePublisher(app.getPublisher(), gen);
    }
    if (app.hasContent()) {
      gen.writeFieldName("content");
      writeContent(app.getContent(), gen);
    }
    if (app.hasKeywords()) {
      gen.writeStringField("keywords", app.getKeywords());
    }
  }

  public final void writeContent(Content content, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeContentFields(content, gen);
    writeExtensions(content, gen);
    gen.writeEndObject();
  }

  @SuppressWarnings("deprecation")
  protected void writeContentFields(Content content, JsonGenerator gen) throws IOException {
    if (content.hasId()) {
      gen.writeStringField("id", content.getId());
    }
    if (content.hasEpisode()) {
      gen.writeNumberField("episode", content.getEpisode());
    }
    if (content.hasTitle()) {
      gen.writeStringField("title", content.getTitle());
    }
    if (content.hasSeries()) {
      gen.writeStringField("series", content.getSeries());
    }
    if (content.hasSeason()) {
      gen.writeStringField("season", content.getSeason());
    }
    if (content.hasProducer()) {
      gen.writeFieldName("producer");
      writeProducer(content.getProducer(), gen);
    }
    if (content.hasUrl()) {
      gen.writeStringField("url", content.getUrl());
    }
    writeContentCategories("cat", content.getCatList(), gen);
    if (content.hasVideoquality()) {
      writeEnumField("videoquality", content.getVideoquality(), gen);
    }
    if (content.hasContext()) {
      writeEnumField("context", content.getContext(), gen);
    }
    if (content.hasContentrating()) {
      gen.writeStringField("contentrating", content.getContentrating());
    }
    if (content.hasUserrating()) {
      gen.writeStringField("userrating", content.getUserrating());
    }
    if (content.hasQagmediarating()) {
      writeEnumField("qagmediarating", content.getQagmediarating(), gen);
    }
    if (content.hasKeywords()) {
      gen.writeStringField("keywords", content.getKeywords());
    }
    if (content.hasLivestream()) {
      writeIntBoolField("livestream", content.getLivestream(), gen);
    }
    if (content.hasSourcerelationship()) {
      writeIntBoolField("sourcerelationship", content.getSourcerelationship(), gen);
    }
    if (content.hasLen()) {
      gen.writeNumberField("len", content.getLen());
    }
    if (content.hasLanguage()) {
      gen.writeStringField("language", content.getLanguage());
    }
    if (content.hasEmbeddable()) {
      writeIntBoolField("embeddable", content.getEmbeddable(), gen);
    }
    if (content.hasArtist()) {
      gen.writeStringField("artist", content.getArtist());
    }
    if (content.hasGenre()) {
      gen.writeStringField("genre", content.getGenre());
    }
    if (content.hasAlbum()) {
      gen.writeStringField("album", content.getAlbum());
    }
    if (content.hasIsrc()) {
      gen.writeStringField("isrc", content.getIsrc());
    }
    if (content.hasProdq()) {
      writeEnumField("prodq", content.getProdq(), gen);
    }
  }

  public final void writeProducer(Producer producer, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeProducerFields(producer, gen);
    writeExtensions(producer, gen);
    gen.writeEndObject();
  }

  protected void writeProducerFields(Producer producer, JsonGenerator gen) throws IOException {
    if (producer.hasId()) {
      gen.writeStringField("id", producer.getId());
    }
    if (producer.hasName()) {
      gen.writeStringField("name", producer.getName());
    }
    writeContentCategories("cat", producer.getCatList(), gen);
    if (producer.hasDomain()) {
      gen.writeStringField("domain", producer.getDomain());
    }
  }

  public final void writePublisher(Publisher publisher, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writePublisherFields(publisher, gen);
    writeExtensions(publisher, gen);
    gen.writeEndObject();
  }

  protected void writePublisherFields(Publisher publisher, JsonGenerator gen) throws IOException {
    if (publisher.hasId()) {
      gen.writeStringField("id", publisher.getId());
    }
    if (publisher.hasName()) {
      gen.writeStringField("name", publisher.getName());
    }
    writeContentCategories("cat", publisher.getCatList(), gen);
    if (publisher.hasDomain()) {
      gen.writeStringField("domain", publisher.getDomain());
    }
  }

  public final void writeDevice(Device device, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeDeviceFields(device, gen);
    writeExtensions(device, gen);
    gen.writeEndObject();
  }

  protected void writeDeviceFields(Device device, JsonGenerator gen) throws IOException {
    if (device.hasUa()) {
      gen.writeStringField("ua", device.getUa());
    }
    if (device.hasGeo()) {
      gen.writeFieldName("geo");
      writeGeo(device.getGeo(), gen);
    }
    if (device.hasDnt()) {
      writeIntBoolField("dnt", device.getDnt(), gen);
    }
    if (device.hasLmt()) {
      writeIntBoolField("lmt", device.getLmt(), gen);
    }
    if (device.hasIp()) {
      gen.writeStringField("ip", device.getIp());
    }
    if (device.hasIpv6()) {
      gen.writeStringField("ipv6", device.getIpv6());
    }
    if (device.hasDevicetype()) {
      writeEnumField("devicetype", device.getDevicetype(), gen);
    }
    if (device.hasMake()) {
      gen.writeStringField("make", device.getMake());
    }
    if (device.hasModel()) {
      gen.writeStringField("model", device.getModel());
    }
    if (device.hasOs()) {
      gen.writeStringField("os", device.getOs());
    }
    if (device.hasOsv()) {
      gen.writeStringField("osv", device.getOsv());
    }
    if (device.hasHwv()) {
      gen.writeStringField("hwv", device.getHwv());
    }
    if (device.hasW()) {
      gen.writeNumberField("w", device.getW());
    }
    if (device.hasH()) {
      gen.writeNumberField("h", device.getH());
    }
    if (device.hasPpi()) {
      gen.writeNumberField("ppi", device.getPpi());
    }
    if (device.hasPxratio()) {
      gen.writeNumberField("pxratio", device.getPxratio());
    }
    if (device.hasJs()) {
      writeIntBoolField("js", device.getJs(), gen);
    }
    if (device.hasFlashver()) {
      gen.writeStringField("flashver", device.getFlashver());
    }
    if (device.hasLanguage()) {
      gen.writeStringField("language", device.getLanguage());
    }
    if (device.hasCarrier()) {
      gen.writeStringField("carrier", device.getCarrier());
    }
    if (device.hasConnectiontype()) {
      writeEnumField("connectiontype", device.getConnectiontype(), gen);
    }
    if (device.hasIfa()) {
      gen.writeStringField("ifa", device.getIfa());
    }
    if (device.hasDidsha1()) {
      gen.writeStringField("didsha1", device.getDidsha1());
    }
    if (device.hasDidmd5()) {
      gen.writeStringField("didmd5", device.getDidmd5());
    }
    if (device.hasDpidsha1()) {
      gen.writeStringField("dpidsha1", device.getDpidsha1());
    }
    if (device.hasDpidmd5()) {
      gen.writeStringField("dpidmd5", device.getDpidmd5());
    }
    if (device.hasMacsha1()) {
      gen.writeStringField("macsha1", device.getMacsha1());
    }
    if (device.hasMacmd5()) {
      gen.writeStringField("macmd5", device.getMacmd5());
    }
    if (device.hasGeofetch()) {
      writeIntBoolField("geofetch", device.getGeofetch(), gen);
    }
    if (device.hasMccmnc()) {
      gen.writeStringField("mccmnc", device.getMccmnc());
    }
  }

  public final void writeGeo(Geo geo, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeGeoFields(geo, gen);
    writeExtensions(geo, gen);
    gen.writeEndObject();
  }

  protected void writeGeoFields(Geo geo, JsonGenerator gen) throws IOException {
    if (geo.hasLat()) {
      gen.writeNumberField("lat", geo.getLat());
    }
    if (geo.hasLon()) {
      gen.writeNumberField("lon", geo.getLon());
    }
    if (geo.hasType()) {
      writeEnumField("type", geo.getType(), gen);
    }
    if (geo.hasCountry()) {
      gen.writeStringField("country", geo.getCountry());
    }
    if (geo.hasRegion()) {
      gen.writeStringField("region", geo.getRegion());
    }
    if (geo.hasRegionfips104()) {
      gen.writeStringField("regionfips104", geo.getRegionfips104());
    }
    if (geo.hasMetro()) {
      gen.writeStringField("metro", geo.getMetro());
    }
    if (geo.hasCity()) {
      gen.writeStringField("city", geo.getCity());
    }
    if (geo.hasZip()) {
      gen.writeStringField("zip", geo.getZip());
    }
    if (geo.hasUtcoffset()) {
      gen.writeNumberField("utcoffset", geo.getUtcoffset());
    }
    if (geo.hasAccuracy()) {
      gen.writeNumberField("accuracy", geo.getAccuracy());
    }
    if (geo.hasLastfix()) {
      gen.writeNumberField("lastfix", geo.getLastfix());
    }
    if (geo.hasIpservice()) {
      writeEnumField("ipservice", geo.getIpservice(), gen);
    }
  }

  public final void writeUser(User user, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeUserFields(user, gen);
    writeExtensions(user, gen);
    gen.writeEndObject();
  }

  protected void writeUserFields(User user, JsonGenerator gen) throws IOException {
    if (user.hasId()) {
      gen.writeStringField("id", user.getId());
    }
    if (user.hasBuyeruid()) {
      gen.writeStringField("buyeruid", user.getBuyeruid());
    }
    if (user.hasYob()) {
      gen.writeNumberField("yob", user.getYob());
    }
    if (user.hasGender() && Gender.forCode(user.getGender()) != null) {
      gen.writeStringField("gender", user.getGender());
    }
    if (user.hasKeywords()) {
      gen.writeStringField("keywords", user.getKeywords());
    }
    if (user.hasCustomdata()) {
      gen.writeStringField("customdata", user.getCustomdata());
    }
    if (user.hasGeo()) {
      gen.writeFieldName("geo");
      writeGeo(user.getGeo(), gen);
    }
    if (user.getDataCount() != 0) {
      gen.writeArrayFieldStart("data");
      for (Data data : user.getDataList()) {
        writeData(data, gen);
      }
      gen.writeEndArray();
    }
  }

  public final void writeData(Data data, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeDataFields(data, gen);
    writeExtensions(data, gen);
    gen.writeEndObject();
  }

  protected void writeDataFields(Data data, JsonGenerator gen) throws IOException {
    if (data.hasId()) {
      gen.writeStringField("id", data.getId());
    }
    if (data.hasName()) {
      gen.writeStringField("name", data.getName());
    }
    if (data.getSegmentCount() != 0) {
      gen.writeArrayFieldStart("segment");
      for (Segment segment : data.getSegmentList()) {
        writeSegment(segment, gen);
      }
      gen.writeEndArray();
    }
  }

  public final void writeSegment(Segment segment, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeSegmentFields(segment, gen);
    writeExtensions(segment, gen);
    gen.writeEndObject();
  }

  protected void writeSegmentFields(Segment segment, JsonGenerator gen) throws IOException {
    if (segment.hasId()) {
      gen.writeStringField("id", segment.getId());
    }
    if (segment.hasName()) {
      gen.writeStringField("name", segment.getName());
    }
    if (segment.hasValue()) {
      gen.writeStringField("value", segment.getValue());
    }
  }

  public final void writeRegs(Regs regs, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeRegsFields(regs, gen);
    writeExtensions(regs, gen);
    gen.writeEndObject();
  }

  protected void writeRegsFields(Regs regs, JsonGenerator gen) throws IOException {
    if (regs.hasCoppa()) {
      writeIntBoolField("coppa", regs.getCoppa(), gen);
    }
  }

  public final void writeSource(Source source, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeSourceFields(source, gen);
    writeExtensions(source, gen);
    gen.writeEndObject();
  }

  protected void writeSourceFields(Source source, JsonGenerator gen) throws IOException {
    if (source.hasFd()) {
      writeIntBoolField("fd", source.getFd(), gen);
    }
    if (source.hasTid()) {
      gen.writeStringField("tid", source.getTid());
    }
    if (source.hasPchain()) {
      gen.writeStringField("pchain", source.getPchain());
    }
  }

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

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

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

  /**
   * Serializes a {@link BidResponse} to JSON, with a provided {@link JsonGenerator}
   * which allows several choices of output and encoding.
   */
  public final void writeBidResponse(BidResponse resp, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeBidResponseFields(resp, gen);
    writeExtensions(resp, gen);
    gen.writeEndObject();
    gen.flush();
  }

  protected void writeBidResponseFields(BidResponse resp, JsonGenerator gen) throws IOException {
    gen.writeStringField("id", resp.getId());
    if (resp.getSeatbidCount() != 0) {
      gen.writeArrayFieldStart("seatbid");
      for (SeatBid seatbid : resp.getSeatbidList()) {
        writeSeatBid(seatbid, gen);
      }
      gen.writeEndArray();
    }
    if (resp.hasBidid()) {
      gen.writeStringField("bidid", resp.getBidid());
    }
    if (resp.hasCur()) {
      gen.writeStringField("cur", resp.getCur());
    }
    if (resp.hasCustomdata()) {
      gen.writeStringField("customdata", resp.getCustomdata());
    }
    if (resp.hasNbr()) {
      writeEnumField("nbr", resp.getNbr(), gen);
    }
  }

  public final void writeSeatBid(SeatBid seatbid, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeSeatBidFields(seatbid, gen);
    writeExtensions(seatbid, gen);
    gen.writeEndObject();
  }

  protected void writeSeatBidFields(SeatBid seatbid, JsonGenerator gen) throws IOException {
    if (seatbid.getBidCount() != 0) {
      gen.writeArrayFieldStart("bid");
      for (Bid bid : seatbid.getBidList()) {
        writeBid(bid, gen);
      }
      gen.writeEndArray();
    }
    if (seatbid.hasSeat()) {
      gen.writeStringField("seat", seatbid.getSeat());
    }
    if (seatbid.hasGroup()) {
      writeIntBoolField("group", seatbid.getGroup(), gen);
    }
  }

  public final void writeBid(Bid bid, JsonGenerator gen) throws IOException {
    gen.writeStartObject();
    writeBidFields(bid, gen);
    writeExtensions(bid, gen);
    gen.writeEndObject();
  }

  protected void writeBidFields(Bid bid, JsonGenerator gen) throws IOException {
    gen.writeStringField("id", bid.getId());
    gen.writeStringField("impid", bid.getImpid());
    gen.writeNumberField("price", bid.getPrice());
    if (bid.hasAdid()) {
      gen.writeStringField("adid", bid.getAdid());
    }
    if (bid.hasNurl()) {
      gen.writeStringField("nurl", bid.getNurl());
    }
    switch (bid.getAdmOneofCase()) {
      case ADM:
        gen.writeStringField("adm", bid.getAdm());
        break;
      case ADM_NATIVE:
        gen.writeFieldName("adm");
        if (factory().isForceNativeAsObject()) {
          nativeWriter().writeNativeResponse(bid.getAdmNative(), gen);
        } else {
          gen.writeString(nativeWriter().writeNativeResponse(bid.getAdmNative()));
        }
        break;
      case ADMONEOF_NOT_SET:
        checkRequired(false);
    }
    writeStrings("adomain", bid.getAdomainList(), gen);
    if (bid.hasBundle()) {
      gen.writeStringField("bundle", bid.getBundle());
    }
    if (bid.hasIurl()) {
      gen.writeStringField("iurl", bid.getIurl());
    }
    if (bid.hasCid()) {
      gen.writeStringField("cid", bid.getCid());
    }
    if (bid.hasCrid()) {
      gen.writeStringField("crid", bid.getCrid());
    }
    writeContentCategories("cat", bid.getCatList(), gen);
    writeEnums("attr", bid.getAttrList(), gen);
    if (bid.hasDealid()) {
      gen.writeStringField("dealid", bid.getDealid());
    }
    if (bid.hasW()) {
      gen.writeNumberField("w", bid.getW());
    }
    if (bid.hasH()) {
      gen.writeNumberField("h", bid.getH());
    }
    if (bid.hasApi()) {
      writeEnumField("api", bid.getApi(), gen);
    }
    if (bid.hasProtocol()) {
      writeEnumField("protocol", bid.getProtocol(), gen);
    }
    if (bid.hasQagmediarating()) {
      writeEnumField("qagmediarating", bid.getQagmediarating(), gen);
    }
    if (bid.hasExp()) {
      gen.writeNumberField("exp", bid.getExp());
    }
    if (bid.hasBurl()) {
      gen.writeStringField("burl", bid.getBurl());
    }
    if (bid.hasLurl()) {
      gen.writeStringField("lurl", bid.getLurl());
    }
    if (bid.hasTactic()) {
      gen.writeStringField("tactic", bid.getTactic());
    }
    if (bid.hasLanguage()) {
      gen.writeStringField("language", bid.getLanguage());
    }
    if (bid.hasWratio()) {
      gen.writeNumberField("wratio", bid.getWratio());
    }
    if (bid.hasHratio()) {
      gen.writeNumberField("hratio", bid.getHratio());
    }
  }

  protected final OpenRtbNativeJsonWriter nativeWriter() {
    if (nativeWriter == null) {
      nativeWriter = factory().newNativeWriter();
    }
    return nativeWriter;
  }
}


/*
 * 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.util;

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

import com.google.common.collect.AbstractIterator;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.openrtb.OpenRtb.BidRequest;
import com.google.openrtb.OpenRtb.BidRequest.Imp;
import com.google.openrtb.OpenRtb.BidRequest.Imp.Banner;
import com.google.openrtb.OpenRtb.BidResponse;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid;
import com.google.openrtb.OpenRtb.BidResponse.SeatBid.Bid;
import com.google.openrtb.OpenRtb.BidResponse.SeatBidOrBuilder;
import com.google.openrtb.OpenRtb.ContentCategory;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;

/**
 * Utilities to manipulate {@link BidRequest} and
 * {@link com.google.openrtb.OpenRtb.BidResponse.Builder}.
 */
public final class OpenRtbUtils {
  /**
   * Special value for the {@code seat} parameter of some methods. Notice that you can't
   * pass any string with the same value, you need to pass a reference to this unique object.
   */
  public static final String SEAT_ANY = "*";
  /**
   * Special value for for {@code impFilter} parameter of some methods, will be more efficient
   * than an equivalent {@code imp -> false} predicate.
   */
  public static final Predicate<Imp> IMP_NONE = imp -> false;
  /**
   * Special value for for {@code impFilter} parameter of some methods, will be more efficient
   * than an equivalent {@code imp -> true} predicate.
   */
  public static final Predicate<Imp> IMP_ALL = imp -> true;

  private static final ImmutableMap<Object, String> CAT_TO_JSON;
  private static final ImmutableMap<String, ContentCategory> NAME_TO_CAT;

  static {
    ImmutableMap.Builder<Object, String> catToJson = ImmutableMap.builder();
    ImmutableMap.Builder<String, ContentCategory> nameToCat = ImmutableMap.builder();
    for (ContentCategory cat : ContentCategory.values()) {
      String json = cat.name().replace('_', '-');
      catToJson.put(cat.name(), json);
      catToJson.put(cat, json);
      nameToCat.put(cat.name(), cat);
      if (!json.equals(cat.name())) {
        catToJson.put(json, json);
        nameToCat.put(json, cat);
      }
    }
    CAT_TO_JSON = catToJson.build();
    NAME_TO_CAT = nameToCat.build();
  }

  /**
   * Get a {@link ContentCategory} from its name (either Java or JSON name).
   */
  @Nullable public static ContentCategory categoryFromName(@Nullable String catName) {
    return NAME_TO_CAT.get(catName);
  }

  /**
   * Get a {@link ContentCategory}'s JSON name, from its Java name.
   */
  @Nullable public static String categoryToJsonName(@Nullable String catName) {
    return CAT_TO_JSON.get(catName);
  }

  /**
   * Get a {@link ContentCategory}'s JSON name.
   */
  @Nullable public static String categoryToJsonName(@Nullable ContentCategory cat) {
    return CAT_TO_JSON.get(cat);
  }

  /**
   * @return The OpenRTB SeatBid with the specified ID; will be created if not existent.
   *     The ID should be present in the request's wseat.
   *
   * @see #seatBid(com.google.openrtb.OpenRtb.BidResponse.Builder)
   * Use for the anonymous seat
   */
  public static SeatBid.Builder seatBid(BidResponse.Builder response, String seat) {
    checkNotNull(seat);

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (seatbid.hasSeat() && seat.equals(seatbid.getSeat())) {
        return seatbid;
      }
    }
    return response.addSeatbidBuilder().setSeat(seat);
  }

  /**
   * @return The anonymous OpenRTB SeatBid, used by non-seat-specific bids (the seat ID is not set).
   *     Will be created if not existent.
   */
  public static SeatBid.Builder seatBid(BidResponse.Builder response) {
    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (!seatbid.hasSeat()) {
        return seatbid;
      }
    }
    return response.addSeatbidBuilder();
  }

  /**
   * Iterates all bids.
   *
   * @return Read-only sequence of all bis in the response.
   *     May have bids from multiple seats, grouped by seat
   */
  public static Iterable<Bid.Builder> bids(BidResponse.Builder response) {
    return new ResponseBidsIterator(response, SEAT_ANY, null);
  }

  /**
   * Iterates all bids from a specific seat.
   *
   * @param seatFilter Filter for seatFilter. You can use {@code null} to select the anonymous seat,
   *     or {@link #SEAT_ANY} to not filter by seat
   * @return View for the seat's internal sequence of bids; or an empty, read-only
   *     view if that seat doesn't exist.
   */
  public static List<Bid.Builder> bids(BidResponse.Builder response, @Nullable String seatFilter) {
    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (filterSeat(seatbid, seatFilter)) {
        return seatbid.getBidBuilderList();
      }
    }
    return ImmutableList.of();
  }

  /**
   * Finds a bid by ID.
   *
   * @param id Bid ID, assumed to be unique within the response
   * @return Matching bid's builder, or {@code null} if not found
   */
  @Nullable public static Bid.Builder bidWithId(BidResponse.Builder response, String id) {
    checkNotNull(id);

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      for (Bid.Builder bid : seatbid.getBidBuilderList()) {
        if (id.equals(bid.getId())) {
          return bid;
        }
      }
    }
    return null;
  }

  /**
   * Finds a bid by seat and ID.
   *
   * @param seatFilter Filter for seatFilter. You can use {@code null} to select the anonymous seat,
   *     or {@link #SEAT_ANY} to not filter by seat
   * @param id Bid ID, assumed to be unique within the filtered seats
   * @return Matching bid's builder, or {@code null} if not found
   */
  @Nullable public static Bid.Builder bidWithId(
      BidResponse.Builder response, @Nullable String seatFilter, String id) {
    checkNotNull(id);

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (filterSeat(seatbid, seatFilter)) {
        for (Bid.Builder bid : seatbid.getBidBuilderList()) {
          if (id.equals(bid.getId())) {
            return bid;
          }
        }
        return null;
      }
    }
    return null;
  }

  /**
   * Finds bids by a custom criteria.
   *
   * @param bidFilter Filter for bids
   * @param seatFilter Filter for seat. You can use {@code null} to select the anonymous seat,
   * or {@link #SEAT_ANY} to not filter by seat
   * @return Read-only sequence of bids that satisfy the filter.
   *     May have bids from multiple seats, grouped by seat
   */
  public static Stream<Bid.Builder> bidStreamWith(
      BidResponse.Builder response, @Nullable String seatFilter,
      @Nullable Predicate<Bid.Builder> bidFilter) {
    return StreamSupport.stream(
        new ResponseBidsIterator(response, seatFilter, bidFilter).spliterator(), false);
  }

  /**
   * Finds bids by a custom criteria.
   *
   * @param bidFilter Filter for bids
   * @param seatFilter Filter for seat. You can use {@code null} to select the anonymous seat,
   * or {@link #SEAT_ANY} to not filter by seat
   * @return Sequence of all bids that satisfy the filter.
   *     May have bids from multiple seats, grouped by seat
   */
  public static Iterable<Bid.Builder> bidsWith(
      BidResponse.Builder response, @Nullable String seatFilter,
      @Nullable Predicate<Bid.Builder> bidFilter) {
    return new ResponseBidsIterator(response, seatFilter, bidFilter);
  }

  /**
   * Updates bids, from all seats.
   *
   * @param updater Update function. The {@code apply()} method can decide or not to update each
   *     object, and it's expected to return {@code true} for objects that were updated
   * @return {@code true} if at least one bid was updated
   * @see ProtoUtils#update(Iterable, Function) for more general updating support
   */
  public static boolean updateBids(
      BidResponse.Builder response, Function<Bid.Builder, Boolean> updater) {
    checkNotNull(updater);

    boolean updated = false;
    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      updated |= ProtoUtils.update(seatbid.getBidBuilderList(), updater);
    }
    return updated;
  }

  /**
   * Updates bids from a given seat.
   *
   * @param seatFilter Seat ID, or {@code null} to select the anonymous seat
   * @param updater Update function. The {@code apply()} method can decide or not to update each
   *     object, and it's expected to return {@code true} for objects that were updated
   * @return {@code true} if at least one bid was updated
   * @see ProtoUtils#update(Iterable, Function) for more general updating support
   */
  public static boolean updateBids(
      BidResponse.Builder response, @Nullable String seatFilter,
      Function<Bid.Builder, Boolean> updater) {
    checkNotNull(updater);

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (filterSeat(seatbid, seatFilter)) {
        return ProtoUtils.update(seatbid.getBidBuilderList(), updater);
      }
    }
    return false;
  }

  /**
   * Remove bids by bid.
   *
   * @param filter Returns {@code true} to keep bid, {@code false} to remove
   * @return {@code true} if any bid was removed
   * @see ProtoUtils#filter(List, Predicate) for more general filtering support
   */
  public static boolean removeBids(BidResponse.Builder response, Predicate<Bid.Builder> filter) {
    checkNotNull(filter);
    boolean updated = false;

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      updated |= removeBids(seatbid, filter);
    }

    return updated;
  }

  private static boolean removeBids(SeatBid.Builder seatbid, Predicate<Bid.Builder> filter) {
    List<Bid.Builder> oldBids = seatbid.getBidBuilderList();
    Iterable<Bid.Builder> newBids = ProtoUtils.filter(oldBids, filter);
    if (newBids == oldBids) {
      return false;
    } else {
      seatbid.clearBid();
      for (Bid.Builder bid : newBids) {
        seatbid.addBid(bid);
      }
      return true;
    }
  }

  /**
   * Remove bids by seat and bid.
   *
   * @param seatFilter Seat ID, or {@code null} to select the anonymous seat
   * @param bidFilter Returns {@code true} to keep bid, {@code false} to remove
   * @return {@code true} if any bid was removed
   * @see ProtoUtils#filter(List, Predicate) for more general filtering support
   */
  public static boolean removeBids(
      BidResponse.Builder response, @Nullable String seatFilter, Predicate<Bid.Builder> bidFilter) {
    checkNotNull(bidFilter);
    boolean updated = false;

    for (SeatBid.Builder seatbid : response.getSeatbidBuilderList()) {
      if (filterSeat(seatbid, seatFilter)) {
        updated |= removeBids(seatbid, bidFilter);
      }
    }

    return updated;
  }

  /**
   * Finds an {@link Imp} by ID.
   *
   * @return The {@link Imp}s that has the given id, or {@code null} if not found.
   */
  @Nullable public static Imp impWithId(BidRequest request, String id) {
    checkNotNull(id);

    for (Imp imp : request.getImpList()) {
      if (imp.getId().equals(id)) {
        return imp;
      }
    }

    return null;
  }

  /**
   * Find an {@link Imp} by its ID and its {@link Banner}'s ID.
   *
   * @param impId Imp ID; optional if the Banner IDs are unique within the request
   * @param bannerId Banner ID
   * @return The {@link Imp} for a given impression ID x banner ID, or {@code null} if not found
   */
  @Nullable public static Imp bannerImpWithId(
      BidRequest request, @Nullable String impId, String bannerId) {
    checkNotNull(bannerId);

    for (Imp imp : request.getImpList()) {
      if ((impId == null || imp.getId().equals(impId))
          && imp.hasBanner() && imp.getBanner().getId().equals(bannerId)) {
        return imp;
      }
    }

    return null;
  }

  /**
   * Optimized code for most filtered lookups. This is worth the effort
   * because bidder code may invoke these lookup methods intensely;
   * common cases like everything-filtered or nothing-filtered are very dominant;
   * and simpler code previously used needed lots of temporary collections.
   *
   * @param request Container of impressions
   * @param impFilter Filters impressions; will be executed exactly once,
   *     and only for impressions that pass the banner/video type filters.
   *     The constants {@link #IMP_NONE} and  {@link #IMP_ALL} allow
   *     more efficient execution when you want to filter none/all impressions.
   * @return Immutable or unmodifiable view for the filtered impressions
   */
  public static Iterable<Imp> impsWith(BidRequest request, Predicate<Imp> impFilter) {
    checkNotNull(impFilter);

    List<Imp> imps = request.getImpList();
    if (imps.isEmpty() || impFilter == IMP_ALL) {
      return imps;
    } else if (impFilter == IMP_NONE) {
      return ImmutableList.of();
    }

    boolean included = impFilter.test(imps.get(0));
    int size = imps.size(), i;

    for (i = 1; i < size; ++i) {
      if (impFilter.test(imps.get(i)) != included) {
        break;
      }
    }

    if (i == size) {
      return included
          ? imps // Unmodifiable, comes from protobuf
          : ImmutableList.<Imp>of();
    }

    int headingSize = i;
    return new FluentIterable<Imp>() {
      @Override public Iterator<Imp> iterator() {
        Iterator<Imp> unfiltered = imps.iterator();
        return new AbstractIterator<Imp>() {
          private int heading = 0;
          @Override protected Imp computeNext() {
            while (unfiltered.hasNext()) {
              Imp imp = unfiltered.next();
              if ((heading++ < headingSize)
                  ? included
                  : impFilter.test(imp)) {
                return imp;
              }
            }
            return endOfData();
        }};
      }};
  }

  public static Stream<Imp> impStreamWith(BidRequest request, Predicate<Imp> impFilter) {
    return StreamSupport.stream(impsWith(request, impFilter).spliterator(), false);
  }

  /**
   * Adds "impression type" subfilters to a base filter, to further restricts impressions
   * that contain a banner, video and/or native object.
   *
   * @param baseFilter base filter for impressions
   * @param banner {@code true} to include impressions with
   * {@link com.google.openrtb.OpenRtb.BidRequest.Imp.Banner}s
   * @param video {@code true} to include impressions with
   * {@link com.google.openrtb.OpenRtb.BidRequest.Imp.Video}s
   * @param nativ {@code true} to include impressions with
   * {@link com.google.openrtb.OpenRtb.BidRequest.Imp.Native}s
   * @return A filter in the form: {@code baseFilter AND (banner OR video OR native)}
   */
  public static Predicate<Imp> addFilters(
      Predicate<Imp> baseFilter, boolean banner, boolean video, boolean nativ) {
    int orCount = (banner ? 1 : 0) + (video ? 1 : 0) + (nativ ? 1 : 0);
    if (baseFilter == IMP_NONE || orCount == 0) {
      return baseFilter;
    }

    Predicate<Imp> typeFilter = null;
    if (banner) {
      typeFilter = Imp::hasBanner;
    }
    if (video) {
      typeFilter = typeFilter == null ? Imp::hasVideo : typeFilter.or(Imp::hasVideo);
    }
    if (nativ) {
      typeFilter = typeFilter == null ? Imp::hasNative : typeFilter.or(Imp::hasNative);
    }

    return baseFilter == IMP_ALL ? typeFilter : baseFilter.and(typeFilter);
  }

  /**
   * Performs a filter by seat.
   *
   * @param seatbid Seat to filter
   * @param seatFilter Filter for seat. You can use {@code null} to select the anonymous seat,
   * or {@link #SEAT_ANY} to not filter by seat
   * @return {@code true} if the seat passes the filter
   */
  public static boolean filterSeat(SeatBidOrBuilder seatbid, @Nullable String seatFilter) {
    return seatFilter == null
        ? !seatbid.hasSeat()
        : seatFilter == SEAT_ANY || seatFilter.equals(seatbid.getSeat());
  }
}