/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.elasticsearch.gson.impl; import java.util.Arrays; import org.hibernate.search.elasticsearch.impl.JsonBuilder; import org.hibernate.search.exception.AssertionFailure; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; /** * A class that abstracts the ways of accessing values in a JSON tree. * * @author Yoann Rodiere */ public abstract class JsonAccessor { /** * Get the current value of the element this accessor points to for the given {@code root}. * * @param root The root to be accessed. * @return The current value pointed to by this accessor on the {@code root}, * or null if it doesn't exist. */ public abstract JsonElement get(JsonObject root); /** * Set the given value on the element this accessor points to for the given {@code root}. * * @param root The root to be accessed. * @param newValue The value to set. * @throws UnexpectedJsonElementTypeException If an element in the path has unexpected type, preventing * access to the element this accessor points to. */ public abstract void set(JsonObject root, JsonElement newValue) throws UnexpectedJsonElementTypeException; /** * Add the given primitive value to the element this accessor points to for the * given {@code root}. * * <p>This method differs from {@link #set(JsonObject, JsonElement)}: * <ul> * <li>If there is currently no value, the given value is simply * {@link #set(JsonObject, JsonElement) set}. * <li>If the current value is an array, the given value is added to this array. * <li>If the current value is a primitive or {@link JsonNull}, the current value * is replaced by an array containing the current value followed by the given value. * <li>Otherwise (i.e. if the current value is an object), an * {@link UnexpectedJsonElementTypeException} is thrown. * </ul> * * @param root The root to be accessed. * @param newValue The value to add. * @throws UnexpectedJsonElementTypeException If an element in the path has unexpected type, preventing * write access to the element this accessor points to. */ public abstract void add(JsonObject root, JsonPrimitive newValue) throws UnexpectedJsonElementTypeException; /** * Get the current value of the lement this accessor points to for the given {@code root}, * creating it and setting it if it hasn't been set yet. * * @param root The root to be accessed. * @param type The expected {@link JsonElementType}. * @return The current value pointed to by this accessor on the {@code root}, always non-null * and typed according to {@code type}. * @throws UnexpectedJsonElementTypeException if the element already exists and is not of type {@code type}, or * if an element in the path has unexpected type, preventing access to the element this accessor * points to. */ public abstract <T extends JsonElement> T getOrCreate(JsonObject root, JsonElementType<T> type) throws UnexpectedJsonElementTypeException; /** * @return The absolute path representing this accessor, excluding runtime details such as array indices. * {@code null} for the root accessor. */ public abstract String getStaticAbsolutePath(); public static JsonAccessor root() { return ROOT; } private static final JsonAccessor ROOT = new JsonAccessor() { @Override public JsonElement get(JsonObject root) { if ( root == null ) { throw new AssertionFailure( "A null root was encountered" ); } return root; } @Override public void set(JsonObject root, JsonElement value) { throw new UnsupportedOperationException( "Cannot set the root element" ); } @Override public <T extends JsonElement> T getOrCreate(JsonObject root, JsonElementType<T> type) { return type.cast( get( root ) ); } @Override public void add(JsonObject root, JsonPrimitive value) { throw new UnsupportedOperationException( "Cannot add a value to the root element" ); } @Override public String toString() { return "root"; } @Override public String getStaticAbsolutePath() { return null; } }; private abstract static class NonRootAccessor<P extends JsonElement> extends JsonAccessor { private final JsonAccessor parentAccessor; public NonRootAccessor(JsonAccessor parentAccessor) { super(); this.parentAccessor = parentAccessor; } protected abstract JsonElementType<P> getExpectedParentType(); @Override public JsonElement get(JsonObject root) { P parent = getExpectedParentType().cast( parentAccessor.get( root ) ); if ( parent == null ) { return null; } return doGet( parent ); } protected abstract JsonElement doGet(P parent); @Override public void set(JsonObject root, JsonElement newValue) throws UnexpectedJsonElementTypeException { P parent = parentAccessor.getOrCreate( root, getExpectedParentType() ); doSet( parent, newValue ); } protected abstract void doSet(P parent, JsonElement newValue); @Override public <T extends JsonElement> T getOrCreate(JsonObject root, JsonElementType<T> type) throws UnexpectedJsonElementTypeException { P parent = parentAccessor.getOrCreate( root, getExpectedParentType() ); JsonElement currentValue = doGet( parent ); if ( currentValue == null || currentValue.isJsonNull() ) { T result = type.newInstance(); doSet( parent, result ); return result; } else if ( !type.isInstance( currentValue ) ) { throw new UnexpectedJsonElementTypeException( this, type, currentValue ); } else { return type.cast( currentValue ); } } @Override public void add(JsonObject root, JsonPrimitive newValue) throws UnexpectedJsonElementTypeException { P parent = parentAccessor.getOrCreate( root, getExpectedParentType() ); JsonElement currentValue = doGet( parent ); if ( currentValue == null ) { // Do not overwrite JsonNull, because it might be there on purpose doSet( parent, newValue ); } else if ( JsonElementType.ARRAY.isInstance( currentValue ) ) { currentValue.getAsJsonArray().add( newValue ); } else if ( JsonElementType.PRIMITIVE.isInstance( currentValue ) || JsonElementType.NULL.isInstance( currentValue ) ) { doSet( parent, JsonBuilder.array().add( currentValue ).add( newValue ).build() ); } else { throw new UnexpectedJsonElementTypeException( this, Arrays.asList( JsonElementType.ARRAY, JsonElementType.PRIMITIVE, JsonElementType.NULL ), currentValue ); } } @Override public String toString() { StringBuilder path = new StringBuilder(); if ( parentAccessor != ROOT ) { path.append( parentAccessor.toString() ); } appendRuntimeRelativePath( path ); return path.toString(); } protected abstract void appendRuntimeRelativePath(StringBuilder path); @Override public String getStaticAbsolutePath() { StringBuilder path = new StringBuilder(); boolean isFirst; if ( parentAccessor == ROOT ) { isFirst = true; } else { isFirst = false; path.append( parentAccessor.getStaticAbsolutePath() ); } appendStaticRelativePath( path, isFirst ); return path.toString(); } protected abstract void appendStaticRelativePath(StringBuilder path, boolean first); } public JsonAccessor property(String propertyName) { return new ObjectPropertyJsonAccessor( this, propertyName ); } private static class ObjectPropertyJsonAccessor extends NonRootAccessor<JsonObject> { private final String propertyName; public ObjectPropertyJsonAccessor(JsonAccessor parentAccessor, String propertyName) { super( parentAccessor ); this.propertyName = propertyName; } @Override protected JsonElementType<JsonObject> getExpectedParentType() { return JsonElementType.OBJECT; } @Override protected JsonElement doGet(JsonObject parent) { return parent.get( propertyName ); } @Override protected void doSet(JsonObject parent, JsonElement newValue) { parent.add( propertyName, newValue ); } @Override protected void appendRuntimeRelativePath(StringBuilder path) { path.append( "." ).append( propertyName ); } @Override protected void appendStaticRelativePath(StringBuilder path, boolean isFirst) { if ( !isFirst ) { path.append( "." ); } path.append( propertyName ); } } public JsonAccessor element(int index) { return new ArrayElementJsonAccessor( this, index ); } private static class ArrayElementJsonAccessor extends NonRootAccessor<JsonArray> { private final int index; public ArrayElementJsonAccessor(JsonAccessor parentAccessor, int index) { super( parentAccessor ); this.index = index; } @Override protected JsonElementType<JsonArray> getExpectedParentType() { return JsonElementType.ARRAY; } @Override protected JsonElement doGet(JsonArray parent) { if ( parent != null && index < parent.size() ) { return parent.get( index ); } else { return null; } } @Override protected void doSet(JsonArray parent, JsonElement newValue) { fillTo( parent, index ); parent.set( index, newValue ); } private static void fillTo(JsonArray array, int index) { for ( int i = array.size(); i <= index; ++i ) { array.add( JsonNull.INSTANCE ); } } @Override protected void appendRuntimeRelativePath(StringBuilder path) { path.append( "[" ).append( index ).append( "]" ); } @Override protected void appendStaticRelativePath(StringBuilder path, boolean first) { // Nothing to do } } }