From bbc059a6897793880ed7fa4922cf9c3016b02f83 Mon Sep 17 00:00:00 2001 From: KevinDockx Date: Fri, 10 Jul 2015 11:41:44 +0200 Subject: [PATCH] Some more JsonPatch refactoring for non-generics - Add Apply to non-generic Operation (used by non-generic JsonPatchDocument) - Add non-generic JsonPatchDocument --- .../Converters/JsonPatchDocumentConverter.cs | 76 +++++++++ .../TypedJsonPatchDocumentConverter.cs | 21 +-- .../Helpers/PathHelpers.cs | 31 ++++ .../IJsonPatchDocument.cs | 2 +- .../JsonPatchDocument.cs | 159 ++++++++++++++++++ .../JsonPatchDocumentOfT.cs | 105 +++++++----- .../Operations/Operation.cs | 34 ++++ .../Operations/OperationOfT.cs | 6 - .../Properties/Resources.Designer.cs | 83 +++++++++ src/Microsoft.AspNet.JsonPatch/Resources.resx | 15 ++ 10 files changed, 461 insertions(+), 71 deletions(-) create mode 100644 src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..4551f64043 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Converters +{ + public class JsonPatchDocumentConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + if (objectType != typeof(JsonPatchDocument)) + { + throw new ArgumentException(Resources.FormatParameterMustMatchType("objectType", "JsonPatchDocument"), "objectType"); + } + + try + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + // load jObject + var jObject = JArray.Load(reader); + + // Create target object for Json => list of operations + var targetOperations = new List(); + + // Create a new reader for this jObject, and set all properties + // to match the original reader. + var jObjectReader = jObject.CreateReader(); + jObjectReader.Culture = reader.Culture; + jObjectReader.DateParseHandling = reader.DateParseHandling; + jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; + jObjectReader.FloatParseHandling = reader.FloatParseHandling; + + // Populate the object properties + serializer.Populate(jObjectReader, targetOperations); + + // container target: the JsonPatchDocument. + var container = new JsonPatchDocument(targetOperations, new DefaultContractResolver()); + + return container; + } + catch (Exception ex) + { + throw new JsonPatchException(Resources.FormatInvalidJsonPatchDocument(objectType.Name), ex); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is IJsonPatchDocument) + { + var jsonPatchDoc = (IJsonPatchDocument)value; + var lst = jsonPatchDoc.GetOperations(); + + // write out the operations, no envelope + serializer.Serialize(writer, lst); + } + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs index 770f629ebf..5f0eb8ac09 100644 --- a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs @@ -12,13 +12,8 @@ using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch.Converters { - public class TypedJsonPatchDocumentConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return true; - } - + public class TypedJsonPatchDocumentConverter : JsonPatchDocumentConverter + { public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { @@ -63,17 +58,5 @@ namespace Microsoft.AspNet.JsonPatch.Converters throw new JsonPatchException(Resources.FormatInvalidJsonPatchDocument(objectType.Name), ex); } } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is IJsonPatchDocument) - { - var jsonPatchDoc = (IJsonPatchDocument)value; - var lst = jsonPatchDoc.GetOperations(); - - // write out the operations, no envelope - serializer.Serialize(writer, lst); - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs new file mode 100644 index 0000000000..28683e7c98 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.JsonPatch.Exceptions; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + internal static class PathHelpers + { + internal static string NormalizePath(string path) + { + // check for most common path errors on create. This is not + // absolutely necessary, but it allows us to already catch mistakes + // on creation of the patch document rather than on execute. + + if (path.Contains(".") || path.Contains("//") || path.Contains(" ") || path.Contains("\\")) + { + throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null); + } + + if (!(path.StartsWith("/"))) + { + return "/" + path; + } + else + { + return path; + } + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs index af807cfd8d..18033e82f0 100644 --- a/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs +++ b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs @@ -11,6 +11,6 @@ namespace Microsoft.AspNet.JsonPatch { IContractResolver ContractResolver { get; set; } - List GetOperations(); + IList GetOperations(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs new file mode 100644 index 0000000000..74d4071530 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Adapters; +using Microsoft.AspNet.JsonPatch.Converters; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Operations; +using Microsoft.Framework.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + // Implementation details: the purpose of this type of patch document is to allow creation of such + // documents for cases where there's no class/DTO to work on. Typical use case: backend not built in + // .NET or architecture doesn't contain a shared DTO layer. + [JsonConverter(typeof(JsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument + { + public List Operations { get; private set; } + + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } + + public JsonPatchDocument() + { + Operations = new List(); + ContractResolver = new DefaultContractResolver(); + } + + public JsonPatchDocument([NotNull] List operations, [NotNull] IContractResolver contractResolver) + { + Operations = operations; + ContractResolver = contractResolver; + } + + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// target location + /// value + /// + public JsonPatchDocument Add([NotNull] string path, object value) + { + Operations.Add(new Operation("add", PathHelpers.NormalizePath(path), null, value)); + return this; + } + + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// target location + /// + public JsonPatchDocument Remove([NotNull] string path) + { + Operations.Add(new Operation("remove", PathHelpers.NormalizePath(path), null, null)); + return this; + } + + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Replace([NotNull] string path, object value) + { + Operations.Add(new Operation("replace", PathHelpers.NormalizePath(path), null, value)); + return this; + } + + /// + /// Removes value at specified location and add it to the target location. Will result in, for example: + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Move([NotNull] string from, [NotNull] string path) + { + Operations.Add(new Operation("move", PathHelpers.NormalizePath(path), PathHelpers.NormalizePath(from))); + return this; + } + + /// + /// Copy the value at specified location to the target location. Willr esult in, for example: + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy([NotNull] string from, [NotNull] string path) + { + Operations.Add(new Operation("copy", PathHelpers.NormalizePath(path), PathHelpers.NormalizePath(from))); + return this; + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo([NotNull] object objectToApplyTo) + { + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo([NotNull] object objectToApplyTo, Action logErrorAction) + { + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo([NotNull] object objectToApplyTo, [NotNull] IObjectAdapter adapter) + { + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + IList IJsonPatchDocument.GetOperations() + { + var allOps = new List(); + + if (Operations != null) + { + foreach (var op in Operations) + { + var untypedOp = new Operation(); + + untypedOp.op = op.op; + untypedOp.value = op.value; + untypedOp.path = op.path; + untypedOp.from = op.from; + + allOps.Add(untypedOp); + } + } + + return allOps; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs index b2ce01708c..75a7508945 100644 --- a/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNet.JsonPatch /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } /// /// value type - /// path + /// target location /// value /// public JsonPatchDocument Add([NotNull] Expression> path, TProp value) @@ -62,7 +62,7 @@ namespace Microsoft.AspNet.JsonPatch /// Add value to list at given position /// /// value type - /// path + /// target location /// value /// position /// @@ -84,7 +84,7 @@ namespace Microsoft.AspNet.JsonPatch /// At value at end of list /// /// value type - /// path + /// target location /// value /// public JsonPatchDocument Add([NotNull] Expression>> path, TProp value) @@ -102,8 +102,7 @@ namespace Microsoft.AspNet.JsonPatch /// Remove value at target location. Will result in, for example, /// { "op": "remove", "path": "/a/b/c" } /// - /// - /// + /// target location /// public JsonPatchDocument Remove([NotNull] Expression> path) { @@ -149,8 +148,8 @@ namespace Microsoft.AspNet.JsonPatch /// Replace value. Will result in, for example, /// { "op": "replace", "path": "/a/b/c", "value": 42 } /// - /// - /// + /// target location + /// value /// public JsonPatchDocument Replace([NotNull] Expression> path, TProp value) { @@ -168,6 +167,7 @@ namespace Microsoft.AspNet.JsonPatch /// /// value type /// target location + /// value /// position /// public JsonPatchDocument Replace([NotNull] Expression>> path, @@ -187,6 +187,7 @@ namespace Microsoft.AspNet.JsonPatch /// /// value type /// target location + /// value /// public JsonPatchDocument Replace([NotNull] Expression>> path, TProp value) { @@ -203,8 +204,8 @@ namespace Microsoft.AspNet.JsonPatch /// Removes value at specified location and add it to the target location. Will result in, for example: /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } /// - /// - /// + /// source location + /// target location /// public JsonPatchDocument Move( [NotNull] Expression> from, @@ -222,9 +223,9 @@ namespace Microsoft.AspNet.JsonPatch /// Move from a position in a list to a new location /// /// - /// - /// - /// + /// source location + /// position + /// target location /// public JsonPatchDocument Move( [NotNull] Expression>> from, @@ -243,9 +244,9 @@ namespace Microsoft.AspNet.JsonPatch /// Move from a property to a location in a list /// /// - /// - /// - /// + /// source location + /// target location + /// position /// public JsonPatchDocument Move( [NotNull] Expression> from, @@ -264,9 +265,10 @@ namespace Microsoft.AspNet.JsonPatch /// Move from a position in a list to another location in a list /// /// - /// - /// - /// + /// source location + /// position (source) + /// target location + /// position (target) /// public JsonPatchDocument Move( [NotNull] Expression>> from, @@ -286,9 +288,9 @@ namespace Microsoft.AspNet.JsonPatch /// Move from a position in a list to the end of another list /// /// - /// - /// - /// + /// source location + /// position + /// target location /// public JsonPatchDocument Move( [NotNull] Expression>> from, @@ -307,9 +309,8 @@ namespace Microsoft.AspNet.JsonPatch /// Move to the end of a list /// /// - /// - /// - /// + /// source location + /// target location /// public JsonPatchDocument Move( [NotNull] Expression> from, @@ -327,8 +328,8 @@ namespace Microsoft.AspNet.JsonPatch /// Copy the value at specified location to the target location. Willr esult in, for example: /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// - /// - /// + /// source location + /// target location /// public JsonPatchDocument Copy( [NotNull] Expression> from, @@ -346,9 +347,9 @@ namespace Microsoft.AspNet.JsonPatch /// Copy from a position in a list to a new location /// /// - /// - /// - /// + /// source location + /// position + /// target location /// public JsonPatchDocument Copy( [NotNull] Expression>> from, @@ -367,9 +368,9 @@ namespace Microsoft.AspNet.JsonPatch /// Copy from a property to a location in a list /// /// - /// - /// - /// + /// source location + /// target location + /// position /// public JsonPatchDocument Copy( [NotNull] Expression> from, @@ -388,9 +389,10 @@ namespace Microsoft.AspNet.JsonPatch /// Copy from a position in a list to a new location in a list /// /// - /// - /// - /// + /// source location + /// position (source) + /// target location + /// position (target) /// public JsonPatchDocument Copy( [NotNull] Expression>> from, @@ -410,9 +412,9 @@ namespace Microsoft.AspNet.JsonPatch /// Copy from a position in a list to the end of another list /// /// - /// - /// - /// + /// source location + /// position + /// target location /// public JsonPatchDocument Copy( [NotNull] Expression>> from, @@ -431,9 +433,8 @@ namespace Microsoft.AspNet.JsonPatch /// Copy to the end of a list /// /// - /// - /// - /// + /// source location + /// target location /// public JsonPatchDocument Copy( [NotNull] Expression> from, @@ -447,17 +448,31 @@ namespace Microsoft.AspNet.JsonPatch return this; } - public void ApplyTo(TModel objectToApplyTo) + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo([NotNull] TModel objectToApplyTo) { ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); } - public void ApplyTo(TModel objectToApplyTo, Action logErrorAction) + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo([NotNull] TModel objectToApplyTo, Action logErrorAction) { ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction)); } - public void ApplyTo(TModel objectToApplyTo, IObjectAdapter adapter) + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo([NotNull] TModel objectToApplyTo, [NotNull] IObjectAdapter adapter) { // apply each operation in order foreach (var op in Operations) @@ -466,7 +481,7 @@ namespace Microsoft.AspNet.JsonPatch } } - public List GetOperations() + IList IJsonPatchDocument.GetOperations() { var allOps = new List(); diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs b/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs index f28357ec7d..b115465fc0 100644 --- a/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs +++ b/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using Microsoft.AspNet.JsonPatch.Adapters; using Microsoft.Framework.Internal; using Newtonsoft.Json; @@ -28,5 +30,37 @@ namespace Microsoft.AspNet.JsonPatch.Operations } + public void Apply([NotNull] object objectToApplyTo, [NotNull] IObjectAdapter adapter) + { + switch (OperationType) + { + case OperationType.Add: + adapter.Add(this, objectToApplyTo); + break; + case OperationType.Remove: + adapter.Remove(this, objectToApplyTo); + break; + case OperationType.Replace: + adapter.Replace(this, objectToApplyTo); + break; + case OperationType.Move: + adapter.Move(this, objectToApplyTo); + break; + case OperationType.Copy: + adapter.Copy(this, objectToApplyTo); + break; + case OperationType.Test: + throw new NotSupportedException(Resources.TestOperationNotSupported); + default: + break; + } + } + + public bool ShouldSerializevalue() + { + return (OperationType == OperationType.Add + || OperationType == OperationType.Replace + || OperationType == OperationType.Test); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs b/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs index 7a72bcc68b..9761a269a1 100644 --- a/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs @@ -52,11 +52,5 @@ namespace Microsoft.AspNet.JsonPatch.Operations } } - public bool ShouldSerializevalue() - { - return (OperationType == OperationType.Add - || OperationType == OperationType.Replace - || OperationType == OperationType.Test); - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs index d9f8a7ee41..15fdcb6194 100644 --- a/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs @@ -122,6 +122,89 @@ namespace Microsoft.AspNet.JsonPatch return GetString("TestOperationNotSupported"); } + /// + /// objectType must be of type JsonPatchDocument. + /// + internal static string ParameterMustMatchType + { + get { return GetString("ParameterMustMatchType"); } + } + + /// + /// objectType must be of type JsonPatchDocument. + /// + internal static string FormatParameterMustMatchType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParameterMustMatchType"), p0, p1); + } + + /// + /// The property at '{0}' could not be read. + /// + internal static string CannotReadProperty + { + get { return GetString("CannotReadProperty"); } + } + + /// + /// The property at '{0}' could not be read. + /// + internal static string FormatCannotReadProperty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotReadProperty"), p0); + } + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string InvalidValueForPath + { + get { return GetString("InvalidValueForPath"); } + } + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string FormatInvalidValueForPath(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForPath"), p0); + } + + + /// + /// The property at path '{0}' could not be removed. + /// + internal static string PropertyCannotBeRemoved + { + get { return GetString("PropertyCannotBeRemoved"); } + } + + /// + /// The property at path '{0}' could not be removed. + /// + internal static string FormatPropertyCannotBeRemoved(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeRemoved"), p0); + } + + /// + /// The property at path '{0}' could not be added. + /// + internal static string PropertyCannotBeAdded + { + get { return GetString("PropertyCannotBeAdded"); } + } + + /// + /// The property at path '{0}' could not be added. + /// + internal static string FormatPropertyCannotBeAdded(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeAdded"), p0); + } + + + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.JsonPatch/Resources.resx b/src/Microsoft.AspNet.JsonPatch/Resources.resx index f6dedfd5f7..3718f837f9 100644 --- a/src/Microsoft.AspNet.JsonPatch/Resources.resx +++ b/src/Microsoft.AspNet.JsonPatch/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The property at '{0}' could not be read. + The property at path '{0}' could not be updated. @@ -129,9 +132,21 @@ For operation '{0}', the provided path is invalid for array property at path '{1}'. + + The provided string '{0}' is an invalid path. + The value '{0}' is invalid for property at path '{1}'. + + '{0}' must be of type '{1}'. + + + The property at path '{0}' could not be added. + + + The property at path '{0}' could not be removed. + Property does not exist at path '{0}'.