content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     https://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 org.apache.avro;
  
  import java.util.AbstractSet;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Queue;
  import java.util.Objects;
  import java.util.Set;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentLinkedQueue;
  import java.util.concurrent.ConcurrentMap;
  
  import java.io.IOException;
  import java.util.function.BiConsumer;
  
  import org.apache.avro.util.internal.Accessor;
  import org.apache.avro.util.internal.Accessor.JsonPropertiesAccessor;
  import org.apache.avro.util.MapEntry;
  import org.apache.avro.util.internal.JacksonUtils;
  
  import com.fasterxml.jackson.core.JsonGenerator;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.node.TextNode;
  
  /**
   * Base class for objects that have JSON-valued properties. Avro and JSON values
   * are represented in Java using the following mapping:
   *
   * <table>
   * <th>
   * <td>Avro type</td>
   * <td>JSON type</td>
   * <td>Java type</td></th>
   * <tr>
   * <td><code>null</code></td>
   * <td><code>null</code></td>
   * <td>{@link #NULL_VALUE}</td>
   * </tr>
   * <tr>
   * <td><code>boolean</code></td>
   * <td>Boolean</td>
   * <td><code>boolean</code></td>
   * </tr>
   * <tr>
   * <td><code>int</code></td>
   * <td>Number</td>
   * <td><code>int</code></td>
   * </tr>
   * <tr>
   * <td><code>long</code></td>
   * <td>Number</td>
   * <td><code>long</code></td>
   * </tr>
   * <tr>
   * <td><code>float</code></td>
   * <td>Number</td>
   * <td><code>float</code></td>
   * </tr>
   * <tr>
   * <td><code>double</code></td>
   * <td>Number</td>
   * <td><code>double</code></td>
   * </tr>
   * <tr>
   * <td><code>bytes</code></td>
   * <td>String</td>
   * <td><code>byte[]</code></td>
   * </tr>
   * <tr>
   * <td><code>string</code></td>
   * <td>String</td>
   * <td>{@link java.lang.String}</td>
   * </tr>
   * <tr>
   * <td><code>record</code></td>
   * <td>Object</td>
   * <td>{@link java.util.Map}</td>
   * </tr>
   * <tr>
   * <td><code>enum</code></td>
   * <td>String</td>
   * <td>{@link java.lang.String}</td>
   * </tr>
   * <tr>
   * <td><code>array</code></td>
   * <td>Array</td>
   * <td>{@link java.util.Collection}</td>
   * </tr>
   * <tr>
   * <td><code>map</code></td>
   * <td>Object</td>
   * <td>{@link java.util.Map}</td>
   * </tr>
   * <tr>
   * <td><code>fixed</code></td>
   * <td>String</td>
   * <td><code>byte[]</code></td>
   * </tr>
   * </table>
   *
   * @see org.apache.avro.data.Json
   */
  public abstract class JsonProperties {
  
    static {
      Accessor.setAccessor(new JsonPropertiesAccessor() {
        @Override
        protected void addProp(JsonProperties props, String name, JsonNode value) {
          props.addProp(name, value);
        }
      });
    }
  
    public static class Null {
      private Null() {
      }
    }
  
    /** A value representing a JSON <code>null</code>. */
    public static final Null NULL_VALUE = new Null();
  
    // use a ConcurrentHashMap for speed and thread safety, but keep a Queue of the
    // entries to maintain order
    // the queue is always updated after the main map and is thus is potentially a
    // subset of the map.
    // By making props private, we can control access and only implement/override
    // the methods
    // we need. We don't ever remove anything so we don't need to implement the
    // clear/remove functionality.
    // Also, we only ever ADD to the collection, never changing a value, so
    // putWithAbsent is the
    // only modifier
    private final ConcurrentMap<String, JsonNode> props = new ConcurrentHashMap<>() {
      private static final long serialVersionUID = 1L;
      private final Queue<MapEntry<String, JsonNode>> propOrder = new ConcurrentLinkedQueue<>();
  
      @Override
      public JsonNode putIfAbsent(String key, JsonNode value) {
        JsonNode r = super.putIfAbsent(key, value);
        if (r == null) {
          propOrder.add(new MapEntry<>(key, value));
        }
        return r;
      }
  
      @Override
      public JsonNode put(String key, JsonNode value) {
        return putIfAbsent(key, value);
      }
  
      @Override
      public Set<Map.Entry<String, JsonNode>> entrySet() {
        return new AbstractSet<>() {
          @Override
          public Iterator<Map.Entry<String, JsonNode>> iterator() {
            return new Iterator<>() {
              Iterator<MapEntry<String, JsonNode>> it = propOrder.iterator();
  
              @Override
              public boolean hasNext() {
                return it.hasNext();
              }
  
              @Override
              public java.util.Map.Entry<String, JsonNode> next() {
                return it.next();
              }
            };
          }
  
          @Override
          public int size() {
            return propOrder.size();
          }
        };
      }
    };
  
    private final Set<String> reserved;
  
    JsonProperties(Set<String> reserved) {
      this.reserved = reserved;
    }
  
    JsonProperties(Set<String> reserved, Map<String, ?> propMap) {
      this.reserved = reserved;
      for (Entry<String, ?> a : propMap.entrySet()) {
        Object v = a.getValue();
        JsonNode json;
        if (v instanceof String) {
          json = TextNode.valueOf((String) v);
        } else if (v instanceof JsonNode) {
          json = (JsonNode) v;
        } else {
          json = JacksonUtils.toJsonNode(v);
        }
        props.put(a.getKey(), json);
      }
    }
  
    /**
     * Returns the value of the named, string-valued property in this schema.
     * Returns <tt>null</tt> if there is no string-valued property with that name.
     */
    public String getProp(String name) {
      JsonNode value = getJsonProp(name);
      return value != null && value.isTextual() ? value.textValue() : null;
    }
  
    /**
     * Returns the value of the named property in this schema. Returns <tt>null</tt>
     * if there is no property with that name.
     */
    private JsonNode getJsonProp(String name) {
      return props.get(name);
    }
  
    /**
     * Returns the value of the named property in this schema. Returns <tt>null</tt>
     * if there is no property with that name.
     */
    public Object getObjectProp(String name) {
      return JacksonUtils.toObject(props.get(name));
    }
  
    public Object getObjectProp(String name, Object defaultValue) {
      final JsonNode json = props.get(name);
      return json != null ? JacksonUtils.toObject(json) : defaultValue;
    }
  
    /**
     * Adds a property with the given name <tt>name</tt> and value <tt>value</tt>.
     * Neither <tt>name</tt> nor <tt>value</tt> can be <tt>null</tt>. It is illegal
     * to add a property if another with the same name but different value already
     * exists in this schema.
     *
     * @param name  The name of the property to add
     * @param value The value for the property to add
     */
    public void addProp(String name, String value) {
      addProp(name, TextNode.valueOf(value));
    }
  
    public void addProp(String name, Object value) {
      if (value instanceof JsonNode) {
        addProp(name, (JsonNode) value);
      } else {
        addProp(name, JacksonUtils.toJsonNode(value));
      }
    }
  
    public void putAll(JsonProperties np) {
      for (Map.Entry<? extends String, ? extends JsonNode> e : np.props.entrySet())
        addProp(e.getKey(), e.getValue());
    }
  
    /**
     * Adds a property with the given name <tt>name</tt> and value <tt>value</tt>.
     * Neither <tt>name</tt> nor <tt>value</tt> can be <tt>null</tt>. It is illegal
     * to add a property if another with the same name but different value already
     * exists in this schema.
     *
     * @param name  The name of the property to add
     * @param value The value for the property to add
     */
    private void addProp(String name, JsonNode value) {
      if (reserved.contains(name))
        throw new AvroRuntimeException("Can't set reserved property: " + name);
  
      if (value == null)
        throw new AvroRuntimeException("Can't set a property to null: " + name);
  
      JsonNode old = props.putIfAbsent(name, value);
      if (old != null && !old.equals(value)) {
        throw new AvroRuntimeException("Can't overwrite property: " + name);
      }
    }
  
    /**
     * Adds all the props from the specified json properties.
     *
     * @see #getObjectProps()
     */
    public void addAllProps(JsonProperties properties) {
      for (Entry<String, JsonNode> entry : properties.props.entrySet())
        addProp(entry.getKey(), entry.getValue());
    }
  
    /** Return the defined properties as an unmodifiable Map. */
    public Map<String, Object> getObjectProps() {
      Map<String, Object> result = new LinkedHashMap<>();
      for (Map.Entry<String, JsonNode> e : props.entrySet())
        result.put(e.getKey(), JacksonUtils.toObject(e.getValue()));
      return Collections.unmodifiableMap(result);
    }
  
    public boolean propsContainsKey(String key) {
      return this.props.containsKey(key);
    }
  
    public void forEachProperty(BiConsumer<String, Object> consumer) {
      for (Map.Entry<String, JsonNode> entry : this.props.entrySet()) {
        final Object value = JacksonUtils.toObject(entry.getValue());
        consumer.accept(entry.getKey(), value);
      }
    }
  
    void writeProps(JsonGenerator gen) throws IOException {
      for (Map.Entry<String, JsonNode> e : props.entrySet())
        gen.writeObjectField(e.getKey(), e.getValue());
    }
  
    int propsHashCode() {
      return props.hashCode();
    }
  
    boolean propsEqual(JsonProperties np) {
      return Objects.equals(props, np.props);
    }
  
    public boolean hasProps() {
      return !props.isEmpty();
    }
  
  }
  