Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a variant of JsonElementAttributesTable that can be modified in-place, at a cost to reading performance. #118

Merged
merged 13 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class GeoJsonConverterFactory : JsonConverterFactory

private readonly RingOrientationOption _ringOrientationOption;

private readonly bool _allowModifyingAttributesTables;

/// <summary>
/// Creates an instance of this class using the defaults.
/// </summary>
Expand Down Expand Up @@ -79,6 +81,7 @@ public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox,
: this(factory, writeGeometryBBox, idPropertyName, RingOrientationOption.EnforceRfc9746)
{
}

/// <summary>
/// Creates an instance of this class using the provided <see cref="GeometryFactory"/>, the
/// given value for whether or not we should write out a "bbox" for a plain geometry,
Expand All @@ -91,11 +94,29 @@ public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox,
/// <param name="ringOrientationOption"></param>
public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox, string idPropertyName,
RingOrientationOption ringOrientationOption)
: this(factory, writeGeometryBBox, idPropertyName, ringOrientationOption, false)
{
}

/// <summary>
/// Creates an instance of this class using the provided <see cref="GeometryFactory"/>, the
/// given value for whether or not we should write out a "bbox" for a plain geometry,
/// feature and feature collection, and the given "magic" string to signal
/// when an <see cref="IAttributesTable"/> property is actually filling in for a Feature's "id".
/// </summary>
/// <param name="factory"></param>
/// <param name="writeGeometryBBox"></param>
/// <param name="idPropertyName"></param>
/// <param name="ringOrientationOption"></param>
/// <param name="allowModifyingAttributesTables"></param>
airbreather marked this conversation as resolved.
Show resolved Hide resolved
public GeoJsonConverterFactory(GeometryFactory factory, bool writeGeometryBBox, string idPropertyName,
RingOrientationOption ringOrientationOption, bool allowModifyingAttributesTables)
{
_factory = factory;
_writeGeometryBBox = writeGeometryBBox;
_idPropertyName = idPropertyName ?? DefaultIdPropertyName;
_ringOrientationOption = ringOrientationOption;
_allowModifyingAttributesTables = allowModifyingAttributesTables;
}

///<inheritdoc cref="JsonConverter.CanConvert(Type)"/>
Expand All @@ -117,7 +138,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
if (typeof(IFeature).IsAssignableFrom(typeToConvert))
return new StjFeatureConverter(_idPropertyName, _writeGeometryBBox);
if (typeof(IAttributesTable).IsAssignableFrom(typeToConvert))
return new StjAttributesTableConverter(_idPropertyName);
return new StjAttributesTableConverter(_idPropertyName, _allowModifyingAttributesTables);

throw new ArgumentException(nameof(typeToConvert));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text.Json;

namespace NetTopologySuite.Features
{
/// <summary>
/// An <see cref="IAttributesTable"/> that has been <b>partially</b> deserialized to a strongly-
/// typed CLR object model, but which still contains some remnants of the JSON source that
/// produced it which may require the consumer to tell us more about what types they expected.
/// <para/>
/// Due to an intentional limitation in <c>System.Text.Json</c>, there is no way to produce a
/// standalone GeoJSON object that includes enough information to produce an object graph that's
/// complete with nested members of arbitrary types.
/// <para/>
/// In that spirit, this interface allows you to pick up where this library left off and use the
/// Feature's attributes in a more strongly-typed fashion using your own knowledge of whatever
/// internal structure the GeoJSON object is expected to have.
/// </summary>
public interface IPartiallyDeserializedAttributesTable : IAttributesTable
{
/// <summary>
/// Attempts to convert this entire table to a strongly-typed CLR object.
/// <para>
/// Modifications to the result <b>WILL NOT</b> propagate back to this table, or vice-versa.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to convert to.
/// </typeparam>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
bool TryDeserializeJsonObject<T>(JsonSerializerOptions options, out T deserialized);

/// <summary>
/// Attempts to get a strongly-typed CLR object that corresponds to a single property that's
/// present in this table.
/// <para>
/// Modifications to the result <b>WILL NOT</b> propagate back to this table, or vice-versa.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to retrieve.
/// </typeparam>
/// <param name="propertyName">
/// The name of the property in this table to get as the specified type.
/// </param>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
bool TryGetJsonObjectPropertyValue<T>(string propertyName, JsonSerializerOptions options, out T deserialized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;

using NetTopologySuite.IO.Converters;

namespace NetTopologySuite.Features
{
internal sealed class JsonArrayInAttributesTableWrapper : IList<object>, IReadOnlyList<object>
{
private readonly JsonArray _array;

private readonly JsonSerializerOptions _serializerOptions;

public JsonArrayInAttributesTableWrapper(JsonArray array, JsonSerializerOptions serializerOptions)
{
_array = array;
_serializerOptions = serializerOptions;
}

public object this[int index]
{
get => Utility.ObjectFromJsonNode(_array[index], _serializerOptions);
set => _array[index] = Utility.ObjectToJsonNode(value, _serializerOptions);
}

public int Count => _array.Count;

bool ICollection<object>.IsReadOnly => false;

public void Add(object item)
{
_array.Add(Utility.ObjectToJsonNode(item, _serializerOptions));
}

public void Clear()
{
_array.Clear();
}

public bool Contains(object item)
{
foreach (JsonNode node in _array)
{
object obj = Utility.ObjectFromJsonNode(node, _serializerOptions);
if (Equals(item, obj))
{
return true;
}
}

return false;
}

public void CopyTo(object[] array, int arrayIndex)
{
foreach (JsonNode node in _array)
{
array[arrayIndex++] = Utility.ObjectFromJsonNode(node, _serializerOptions);
}
}

public IEnumerator<object> GetEnumerator()
{
return _array.Select(node => Utility.ObjectFromJsonNode(node, _serializerOptions)).GetEnumerator();
}

public int IndexOf(object item)
{
for (int i = 0; i < _array.Count; i++)
{
object obj = Utility.ObjectFromJsonNode(_array[i], _serializerOptions);
if (Equals(item, obj))
{
return i;
}
}

return -1;
}

public void Insert(int index, object item)
{
_array.Insert(index, Utility.ObjectToJsonNode(item, _serializerOptions));
}

public bool Remove(object item)
{
for (int i = 0; i < _array.Count; i++)
{
object obj = Utility.ObjectFromJsonNode(_array[i], _serializerOptions);
if (Equals(item, obj))
{
_array.RemoveAt(i);
return true;
}
}

return false;
}

public void RemoveAt(int index)
{
_array.RemoveAt(index);
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace NetTopologySuite.Features
/// JSON <see cref="JsonValueKind.Object"/>-valued properties are wrapped in their own
/// <see cref="JsonElementAttributesTable"/> objects.
/// </remarks>
public sealed class JsonElementAttributesTable : IAttributesTable
public sealed class JsonElementAttributesTable : IPartiallyDeserializedAttributesTable
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonElementAttributesTable"/> class.
Expand Down Expand Up @@ -119,62 +119,13 @@ public object[] GetValues()
.ToArray();
}

/// <summary>
/// Attempts to convert this table to a strongly-typed value.
/// <para>
/// This is essentially just a way of calling
/// <see cref="JsonSerializer.Deserialize{TValue}(ref Utf8JsonReader, JsonSerializerOptions)"/>
/// on a Feature's <c>"properties"</c> object.
/// </para>
/// <para>
/// <c>System.Text.Json</c> intentionally omits the functionality that would let us do this
/// automatically, for security reasons, so this is the workaround for now.
/// </para>.
/// </summary>
/// <typeparam name="T">
/// The type of object to convert to.
/// </typeparam>.
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
/// <inheritdoc />
public bool TryDeserializeJsonObject<T>(JsonSerializerOptions options, out T deserialized)
{
return TryDeserializeElement(this.RootElement, options, out deserialized);
}

/// <summary>
/// Attempts to get a strongly-typed value for that corresponds to a property of this table.
/// <para>
/// This is essentially just a way of calling
/// <see cref="JsonSerializer.Deserialize{TValue}(ref Utf8JsonReader, JsonSerializerOptions)"/>
/// on one of the individual items from a Feature's <c>"properties"</c>.
/// </para>
/// <para>
/// <c>System.Text.Json</c> intentionally omits the functionality that would let us do this
/// automatically, for security reasons, so this is the workaround for now.
/// </para>
/// </summary>
/// <typeparam name="T">
/// The type of object to retrieve.
/// </typeparam>
/// <param name="propertyName">
/// The name of the property in this table to get as the specified type.
/// </param>
/// <param name="options">
/// The <see cref="JsonSerializerOptions"/> to use for the deserialization.
/// </param>
/// <param name="deserialized">
/// Receives the converted value on success, or the default value on failure.
/// </param>
/// <returns>
/// A value indicating whether or not the conversion succeeded.
/// </returns>
/// <inheritdoc />
public bool TryGetJsonObjectPropertyValue<T>(string propertyName, JsonSerializerOptions options, out T deserialized)
{
if (!this.RootElement.TryGetProperty(propertyName, out var elementToTransform))
Expand Down Expand Up @@ -582,7 +533,7 @@ private static bool TryDeserializeElement<T>(JsonElement elementToTransform, Jso
}
}

private static object ConvertValue(JsonElement prop)
internal static object ConvertValue(JsonElement prop)
{
switch (prop.ValueKind)
{
Expand Down
Loading