diff --git a/src/Beutl.Core/Collections/HierarchicalList.cs b/src/Beutl.Core/Collections/HierarchicalList.cs index 6117ca622..c096a2015 100644 --- a/src/Beutl.Core/Collections/HierarchicalList.cs +++ b/src/Beutl.Core/Collections/HierarchicalList.cs @@ -11,5 +11,10 @@ public HierarchicalList(IModifiableHierarchical parent) Detached += item => parent.RemoveChild(item); } - public IModifiableHierarchical Parent { get; } + public HierarchicalList() + { + ResetBehavior = ResetBehavior.Remove; + } + + public IModifiableHierarchical? Parent { get; } } diff --git a/src/Beutl.Core/CoreObject.cs b/src/Beutl.Core/CoreObject.cs index 827325ccc..bdf3ac60d 100644 --- a/src/Beutl.Core/CoreObject.cs +++ b/src/Beutl.Core/CoreObject.cs @@ -456,6 +456,12 @@ public virtual void Deserialize(ICoreSerializationContext context) Optional value = item.RouteDeserialize(context); if (value.HasValue) { + if (value.Value is IReference { IsNull: false } reference) + { + context.Resolve(reference.Id, + resolved => SetValue(item, reference.Resolved((CoreObject)resolved))); + } + SetValue(item, value.Value); } } diff --git a/src/Beutl.Core/Hierarchy/Hierarchical.cs b/src/Beutl.Core/Hierarchy/Hierarchical.cs index 6cd0e323c..fb6fe9cf7 100644 --- a/src/Beutl.Core/Hierarchy/Hierarchical.cs +++ b/src/Beutl.Core/Hierarchy/Hierarchical.cs @@ -5,6 +5,7 @@ namespace Beutl; +// TODO: 複数の親要素(参照元)を持てるようにする public abstract class Hierarchical : CoreObject, IHierarchical, IModifiableHierarchical { public static readonly CoreProperty HierarchicalParentProperty; diff --git a/src/Beutl.Core/JsonHelper.cs b/src/Beutl.Core/JsonHelper.cs index 1ac17df61..52208f6e5 100644 --- a/src/Beutl.Core/JsonHelper.cs +++ b/src/Beutl.Core/JsonHelper.cs @@ -87,6 +87,7 @@ public static void JsonRestore2(this ICoreSerializable serializable, string file using (ThreadLocalSerializationContext.Enter(context)) { serializable.Deserialize(context); + context.AfterDeserialized(serializable); } } } diff --git a/src/Beutl.Core/OptionalJsonConverter.cs b/src/Beutl.Core/OptionalJsonConverter.cs index 48ddd43e5..a2ef2ced2 100644 --- a/src/Beutl.Core/OptionalJsonConverter.cs +++ b/src/Beutl.Core/OptionalJsonConverter.cs @@ -45,6 +45,7 @@ public override bool CanConvert(Type typeToConvert) using (ThreadLocalSerializationContext.Enter(context)) { serializable.Deserialize(context); + context.AfterDeserialized(serializable); } instance = serializable; diff --git a/src/Beutl.Core/ProjectItemContainer.cs b/src/Beutl.Core/ProjectItemContainer.cs index 095147232..70f49410e 100644 --- a/src/Beutl.Core/ProjectItemContainer.cs +++ b/src/Beutl.Core/ProjectItemContainer.cs @@ -93,7 +93,6 @@ public bool TryGetOrCreateItem(string file, [NotNullWhen(true)] out ProjectItem? public void Add(ProjectItem item) { - _app.Items.Add(item); foreach (WeakReference wref in _items) { if (!wref.TryGetTarget(out _)) diff --git a/src/Beutl.Core/Reference.cs b/src/Beutl.Core/Reference.cs new file mode 100644 index 000000000..9414acf88 --- /dev/null +++ b/src/Beutl.Core/Reference.cs @@ -0,0 +1,83 @@ +namespace Beutl; + +public interface IReference +{ + Guid Id { get; } + + CoreObject? Value { get; } + + bool IsNull { get; } + + Type ObjectType { get; } + + IReference Resolved(CoreObject obj); +} + +public readonly struct Reference : IEquatable>, IReference + where TObject : notnull, CoreObject +{ + private readonly TObject? _value; + private readonly Guid _id; + + public Reference(Guid id) : this(id, null) + { + } + + public Reference(TObject value) : this(value.Id, value) + { + } + + public Reference(Guid id, TObject? value) + { + _id = id; + _value = value; + } + + public Reference() + { + } + + public Guid Id => _value?.Id ?? _id; + + public TObject? Value => _value; + + public bool IsNull => _id == Guid.Empty; + + CoreObject? IReference.Value => Value; + + Type IReference.ObjectType => typeof(TObject); + + public Reference Resolved(TObject obj) + { + return new Reference(obj); + } + + IReference IReference.Resolved(CoreObject obj) + { + return Resolved((TObject)obj); + } + + public void Deconstruct(out Guid Id, out TObject? Value) + { + Id = this.Id; + Value = this.Value; + } + + public bool Equals(Reference other) => Id.Equals(other.Id); + + public override bool Equals(object? obj) => obj is Reference other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(_value, _id); + + public static implicit operator Guid(Reference reference) => reference.Id; + + public static implicit operator Reference(Guid id) => new(id); + + public static implicit operator TObject?(Reference reference) => reference.Value; + + public static implicit operator Reference(TObject value) => new(value.Id, value); + + public static bool operator ==(Reference left, Reference right) => left.Equals(right); + + public static bool operator !=(Reference left, Reference right) => !left.Equals(right); +} diff --git a/src/Beutl.Core/Serialization/CoreSerializableJsonConverter.cs b/src/Beutl.Core/Serialization/CoreSerializableJsonConverter.cs index 7ca2ed3b1..39c677cf6 100644 --- a/src/Beutl.Core/Serialization/CoreSerializableJsonConverter.cs +++ b/src/Beutl.Core/Serialization/CoreSerializableJsonConverter.cs @@ -26,6 +26,7 @@ public sealed class CoreSerializableJsonConverter : JsonConverter callback); } diff --git a/src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs b/src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs index 4dc0b9054..b212c82dd 100644 --- a/src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs +++ b/src/Beutl.Core/Serialization/JsonSerializationContext.Deserialize.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; @@ -25,7 +26,8 @@ private static void DeserializeArray( else { string name = index.ToString(); - output.Add(Deserialize(item, elementType, name, new RelaySerializationErrorNotifier(errorNotifier, name), parent)); + output.Add(Deserialize(item, elementType, name, + new RelaySerializationErrorNotifier(errorNotifier, name), parent)); } index++; @@ -78,6 +80,7 @@ private static void DeserializeArray( if (Activator.CreateInstance(actualType) is ICoreSerializable instance) { instance.Deserialize(context); + context.AfterDeserialized(instance); return instance; } @@ -99,6 +102,12 @@ private static void DeserializeArray( return ArrayTypeHelpers.ConvertArrayType(output, baseType, elementType); } } + else if (node is JsonValue jsonValue + && jsonValue.TryGetValue(out Guid id) + && baseType.IsAssignableTo(typeof(IReference))) + { + return Activator.CreateInstance(baseType, id); + } } ISerializationErrorNotifier? captured = LocalSerializationErrorNotifier.Current; diff --git a/src/Beutl.Core/Serialization/JsonSerializationContext.Serialize.cs b/src/Beutl.Core/Serialization/JsonSerializationContext.Serialize.cs index 69ab0ae6f..0ac3d52c9 100644 --- a/src/Beutl.Core/Serialization/JsonSerializationContext.Serialize.cs +++ b/src/Beutl.Core/Serialization/JsonSerializationContext.Serialize.cs @@ -36,6 +36,10 @@ public partial class JsonSerializationContext return obj; } + else if (value is IReference reference) + { + return reference.Id; + } else if (value is JsonNode jsonNode) { return jsonNode; @@ -115,7 +119,7 @@ public void SetValue(string name, T? value) else { Type actualType = value.GetType(); - if (value is ICoreSerializable or IEnumerable) + if (value is ICoreSerializable or IEnumerable or IReference) { _json[name] = Serialize(name, value, actualType, typeof(T), ErrorNotifier, this); } diff --git a/src/Beutl.Core/Serialization/JsonSerializationContext.cs b/src/Beutl.Core/Serialization/JsonSerializationContext.cs index b1c1aa156..4d5178531 100644 --- a/src/Beutl.Core/Serialization/JsonSerializationContext.cs +++ b/src/Beutl.Core/Serialization/JsonSerializationContext.cs @@ -1,21 +1,31 @@ -using System.Text.Json.Nodes; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; namespace Beutl.Serialization; public partial class JsonSerializationContext( - Type ownerType, ISerializationErrorNotifier errorNotifier, - ICoreSerializationContext? parent = null, JsonObject? json = null) + Type ownerType, + ISerializationErrorNotifier errorNotifier, + ICoreSerializationContext? parent = null, + JsonObject? json = null) : IJsonSerializationContext { public readonly Dictionary _knownTypes = []; + private List<(Guid, Action)>? _resolvers; + private Dictionary? _objects; private readonly JsonObject _json = json ?? []; public ICoreSerializationContext? Parent { get; } = parent; + public JsonSerializationContext Root => IsRoot ? this : (Parent as JsonSerializationContext)!.Root; + public CoreSerializationMode Mode => CoreSerializationMode.ReadWrite; public Type OwnerType { get; } = ownerType; + [MemberNotNullWhen(false, nameof(Parent))] + public bool IsRoot => Parent == null; + public ISerializationErrorNotifier ErrorNotifier { get; } = errorNotifier; public JsonObject GetJsonObject() @@ -57,6 +67,62 @@ public void Populate(string name, ICoreSerializable obj) } } + public void AfterDeserialized(ICoreSerializable obj) + { + if (obj is CoreObject coreObject) + { + SetObjectAndId(coreObject); + + if (IsRoot) + { + // Resolve references + if (_resolvers == null) + return; + + for (int i = _resolvers.Count - 1; i >= 0; i--) + { + var (id, callback) = _resolvers[i]; + if (_objects.TryGetValue(id, out var resolved)) + { + callback(resolved); + } + + _resolvers.RemoveAt(i); + } + + // TODO: アプリケーション全体から解決できるようになれば、 + // ここにそのコードを追加する。 + } + } + } + + [MemberNotNull(nameof(_objects))] + private void SetObjectAndId(CoreObject coreObject) + { + if (IsRoot) + { + _objects ??= new(); + _objects[coreObject.Id] = coreObject; + } + else + { + Root.SetObjectAndId(coreObject); + } + } + + public void Resolve(Guid id, Action callback) + { + if (IsRoot) + { + _resolvers ??= new(); + _resolvers.Add((id, callback)); + } + else + { + Parent.Resolve(id, callback); + } + } + public bool Contains(string name) { return _json.ContainsKey(name); diff --git a/src/Beutl.Engine/Animation/AnimationSerializer.cs b/src/Beutl.Engine/Animation/AnimationSerializer.cs index cda5c648b..f5ef157c5 100644 --- a/src/Beutl.Engine/Animation/AnimationSerializer.cs +++ b/src/Beutl.Engine/Animation/AnimationSerializer.cs @@ -42,6 +42,7 @@ internal static class AnimationSerializer using (ThreadLocalSerializationContext.Enter(innerContext)) { animation.Deserialize(innerContext); + innerContext.AfterDeserialized(animation); } return animation; } diff --git a/src/Beutl.Engine/Converters/BrushJsonConverter.cs b/src/Beutl.Engine/Converters/BrushJsonConverter.cs index 48e827afd..5f5943294 100644 --- a/src/Beutl.Engine/Converters/BrushJsonConverter.cs +++ b/src/Beutl.Engine/Converters/BrushJsonConverter.cs @@ -30,6 +30,7 @@ public override IBrush Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS using (ThreadLocalSerializationContext.Enter(context)) { instance.Deserialize(context); + context.AfterDeserialized(instance); } return brush; diff --git a/src/Beutl.Engine/Converters/KeyFrameJsonConverter.cs b/src/Beutl.Engine/Converters/KeyFrameJsonConverter.cs index 56ae96788..a78890369 100644 --- a/src/Beutl.Engine/Converters/KeyFrameJsonConverter.cs +++ b/src/Beutl.Engine/Converters/KeyFrameJsonConverter.cs @@ -57,6 +57,7 @@ public override IKeyFrame Read(ref Utf8JsonReader reader, Type typeToConvert, Js using (ThreadLocalSerializationContext.Enter(context)) { instance.Deserialize(context); + context.AfterDeserialized(instance); } return instance; diff --git a/src/Beutl.Engine/Converters/PenJsonConverter.cs b/src/Beutl.Engine/Converters/PenJsonConverter.cs index d3277061d..559ba34fd 100644 --- a/src/Beutl.Engine/Converters/PenJsonConverter.cs +++ b/src/Beutl.Engine/Converters/PenJsonConverter.cs @@ -26,6 +26,7 @@ public override IPen Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer using (ThreadLocalSerializationContext.Enter(context)) { pen.Deserialize(context); + context.AfterDeserialized(pen); } return pen; diff --git a/src/Beutl.Engine/Converters/TransformJsonConverter.cs b/src/Beutl.Engine/Converters/TransformJsonConverter.cs index e1d954ad2..c70de9de9 100644 --- a/src/Beutl.Engine/Converters/TransformJsonConverter.cs +++ b/src/Beutl.Engine/Converters/TransformJsonConverter.cs @@ -30,6 +30,7 @@ public override ITransform Read(ref Utf8JsonReader reader, Type typeToConvert, J using (ThreadLocalSerializationContext.Enter(context)) { instance.Deserialize(context); + context.AfterDeserialized(instance); } return transform; diff --git a/src/Beutl.ProjectSystem/NodeTree/Node.cs b/src/Beutl.ProjectSystem/NodeTree/Node.cs index 1a2f0cc98..1c52615a0 100644 --- a/src/Beutl.ProjectSystem/NodeTree/Node.cs +++ b/src/Beutl.ProjectSystem/NodeTree/Node.cs @@ -450,6 +450,7 @@ public override void Deserialize(ICoreSerializationContext context) using (ThreadLocalSerializationContext.Enter(innerContext)) { serializable.Deserialize(innerContext); + innerContext.AfterDeserialized(serializable); } } } diff --git a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupInput.cs b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupInput.cs index bc82a28ff..586a675a4 100644 --- a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupInput.cs +++ b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupInput.cs @@ -95,6 +95,7 @@ public override void Deserialize(ICoreSerializationContext context) using (ThreadLocalSerializationContext.Enter(innerContext)) { serializable.Deserialize(innerContext); + innerContext.AfterDeserialized(serializable); } } diff --git a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupNode.cs b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupNode.cs index dfbf11632..71d3cacba 100644 --- a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupNode.cs +++ b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupNode.cs @@ -337,6 +337,7 @@ public override void Deserialize(ICoreSerializationContext context) using (ThreadLocalSerializationContext.Enter(innerContext)) { nodeItem.Deserialize(innerContext); + innerContext.AfterDeserialized(nodeItem); } ((NodeItem)nodeItem).LocalId = index; diff --git a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupOutput.cs b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupOutput.cs index 1e98a0287..5085269b5 100644 --- a/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupOutput.cs +++ b/src/Beutl.ProjectSystem/NodeTree/Nodes/Group/GroupOutput.cs @@ -95,6 +95,7 @@ public override void Deserialize(ICoreSerializationContext context) using (ThreadLocalSerializationContext.Enter(innerContext)) { serializable.Deserialize(innerContext); + innerContext.AfterDeserialized(serializable); } } diff --git a/src/Beutl.ProjectSystem/NodeTree/Nodes/LayerInputNode.cs b/src/Beutl.ProjectSystem/NodeTree/Nodes/LayerInputNode.cs index 893bb5ddc..48782f717 100644 --- a/src/Beutl.ProjectSystem/NodeTree/Nodes/LayerInputNode.cs +++ b/src/Beutl.ProjectSystem/NodeTree/Nodes/LayerInputNode.cs @@ -160,6 +160,7 @@ public override void Deserialize(ICoreSerializationContext context) using (ThreadLocalSerializationContext.Enter(innerContext)) { serializable.Deserialize(innerContext); + innerContext.AfterDeserialized(serializable); } } diff --git a/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs b/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs index 493784f03..6744427f6 100644 --- a/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs +++ b/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs @@ -686,6 +686,7 @@ public void Undo() using (ThreadLocalSerializationContext.Enter(context)) { _element.Deserialize(context); + context.AfterDeserialized(_element); } _element.Save(_fileName); diff --git a/src/Beutl/Services/ObjectSearcher.cs b/src/Beutl/Services/ObjectSearcher.cs index cda635baa..38b6c52b5 100644 --- a/src/Beutl/Services/ObjectSearcher.cs +++ b/src/Beutl/Services/ObjectSearcher.cs @@ -42,6 +42,14 @@ public IReadOnlyList SearchAll() private object? SearchRecursive(object obj) { + if (obj is IOptional optional) + { + obj = optional.ToObject().GetValueOrDefault()!; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (obj == null) + return null; + } + if (!_hashSet.Add(obj)) return null; @@ -56,7 +64,9 @@ public IReadOnlyList SearchAll() { case CoreObject coreObject: foreach (CoreProperty? item in PropertyRegistry.GetRegistered(coreObject.GetType()) - .Where(x => !x.PropertyType.IsValueType && x != Hierarchical.HierarchicalParentProperty)) + .Where(x => (!x.PropertyType.IsValueType + || x.PropertyType.IsAssignableTo(typeof(IOptional))) + && x.Id != Hierarchical.HierarchicalParentProperty.Id)) { object? value = coreObject.GetValue(item); if (value != null @@ -65,6 +75,7 @@ public IReadOnlyList SearchAll() return result; } } + break; case IEnumerable enm: @@ -82,7 +93,8 @@ public IReadOnlyList SearchAll() case IPropertyAdapter property: { - if (!property.PropertyType.IsValueType + if ((!property.PropertyType.IsValueType + || property.PropertyType.IsAssignableTo(typeof(IOptional))) && property.GetValue() is { } value && SearchRecursive(value) is { } result1) { @@ -122,12 +134,14 @@ private void SearchAllRecursive(object obj, List list) { case CoreObject coreObject: foreach (object? item in PropertyRegistry.GetRegistered(coreObject.GetType()) - .Where(x => !x.PropertyType.IsValueType && x != Hierarchical.HierarchicalParentProperty) - .Select(coreObject.GetValue) - .Where(x => x != null)) + .Where(x => !x.PropertyType.IsValueType && + x != Hierarchical.HierarchicalParentProperty) + .Select(coreObject.GetValue) + .Where(x => x != null)) { SearchAllRecursive(item!, list); } + break; case IEnumerable enm: diff --git a/src/Beutl/Services/ProjectService.cs b/src/Beutl/Services/ProjectService.cs index fe9c33906..0d0e25408 100644 --- a/src/Beutl/Services/ProjectService.cs +++ b/src/Beutl/Services/ProjectService.cs @@ -40,13 +40,14 @@ public ProjectService() using Activity? activity = Telemetry.StartActivity("OpenProject"); try { + CloseProject(); + var project = new Project(); project.Restore(file); - Project? old = _app.Project; _app.Project = project; // 値を発行 - _projectObservable.OnNext((New: project, old)); + _projectObservable.OnNext((New: project, null)); AddToRecentProjects(file); @@ -80,6 +81,8 @@ public void CloseProject() activity?.SetTag(nameof(samplerate), samplerate); try { + CloseProject(); + location = Path.Combine(location, name); var scene = new Scene(width, height, name); ProjectItemContainer.Current.Add(scene); @@ -101,7 +104,7 @@ public void CloseProject() project.Save(projectFile); // 値を発行 - _projectObservable.OnNext((New: project, _app.Project)); + _projectObservable.OnNext((New: project, null)); _app.Project = project; AddToRecentProjects(projectFile); diff --git a/tests/Beutl.UnitTests/Core/JsonSerializationTest.cs b/tests/Beutl.UnitTests/Core/JsonSerializationTest.cs index 7f8ad1cbf..b794b4723 100644 --- a/tests/Beutl.UnitTests/Core/JsonSerializationTest.cs +++ b/tests/Beutl.UnitTests/Core/JsonSerializationTest.cs @@ -1,4 +1,5 @@ -using Beutl.Graphics; +using System.Text.Json.Nodes; +using Beutl.Graphics; using Beutl.Graphics.Shapes; using Beutl.Logging; using Beutl.Media; @@ -8,12 +9,38 @@ using Beutl.Operation; using Beutl.Operators.Source; using Beutl.ProjectSystem; +using Beutl.Serialization; using Microsoft.Extensions.Logging; namespace Beutl.UnitTests.Core; public class JsonSerializationTest { + private class TestSerializable : CoreObject + { + public TestSerializable? Instance { get; set; } + + public TestSerializable? Reference { get; set; } + + public override void Serialize(ICoreSerializationContext context) + { + base.Serialize(context); + context.SetValue(nameof(Instance), Instance); + context.SetValue(nameof(Reference), Reference?.Id); + } + + public override void Deserialize(ICoreSerializationContext context) + { + base.Deserialize(context); + Instance = context.GetValue(nameof(Instance)); + var id = context.GetValue(nameof(Reference)); + if (id.HasValue) + { + context.Resolve(id.Value, o => Reference = o as TestSerializable); + } + } + } + [SetUp] public void Setup() { @@ -51,4 +78,39 @@ public void Serialize() Assert.That(((OutputSocket)rectNode.Items[0]).TryConnect((InputSocket)shapeNode.Items[1])); Assert.That(((OutputSocket)shapeNode.Items[0]).TryConnect((InputSocket)outNode.Items[0])); } + + [Test] + public void Resolve() + { + var original1 = new TestSerializable(); + var original2 = new TestSerializable(); + var original3 = new TestSerializable(); + original1.Instance = original2; + original2.Reference = original3; + original3.Instance = original1; + var json = new JsonObject(); + + var context1 = new JsonSerializationContext(original3.GetType(), NullSerializationErrorNotifier.Instance, json: json); + using (ThreadLocalSerializationContext.Enter(context1)) + { + original3.Serialize(context1); + } + + var restored = new TestSerializable(); + var context2 = new JsonSerializationContext(original3.GetType(), NullSerializationErrorNotifier.Instance, json: json); + using (ThreadLocalSerializationContext.Enter(context2)) + { + restored.Deserialize(context2); + context2.AfterDeserialized(restored); + } + + Assert.That(restored.Id, Is.EqualTo(original3.Id)); + Assert.That(restored.Instance, Is.Not.Null); + Assert.That(restored.Instance!.Id, Is.EqualTo(original1.Id)); + Assert.That(restored.Instance.Reference, Is.Null); + Assert.That(restored.Instance.Instance, Is.Not.Null); + Assert.That(restored.Instance.Instance!.Id, Is.EqualTo(original2.Id)); + Assert.That(restored.Instance.Instance.Reference, Is.Not.Null); + Assert.That(restored.Instance.Instance.Reference!.Id, Is.EqualTo(original3.Id)); + } }