Class DraftPayloadSupport

java.lang.Object
io.jmix.uirecoveryflowui.draft.DraftPayloadSupport

@NullMarked @Component("uirec_DraftPayloadSupport") public class DraftPayloadSupport extends Object
Serializes the dirty state (modified and removed instances) of a DataContext into a JSON payload, and applies such a payload back onto a (possibly different) DataContext.
  • Field Details

  • Constructor Details

    • DraftPayloadSupport

      public DraftPayloadSupport()
  • Method Details

    • createPayload

      public @Nullable String createPayload(DataContext dataContext)
      Serializes the modified and removed instances of the given data context into a JSON payload.
      Parameters:
      dataContext - data context to capture
      Returns:
      JSON payload, or null if the context has no changes
    • warnIfSkipLosesTheChange

      protected void warnIfSkipLosesTheChange(DataContext dataContext, Object entity)
      Warns when skipping a no-primary-key instance actually drops the user's change.

      Normally it does not: an edit to order.address.city is also recorded on the order as the dotted attribute address.city. But the data context records an embedded change one level deep only, so an edit to order.address.geo.lat is recorded on the address and nothing with a primary key carries it. A dotted path among the skipped instance's own modified attributes tells the two cases apart.

    • hasNoPrimaryKey

      protected boolean hasNoPrimaryKey(Object entity)
      True when the given entity has no primary key: a JPA @Embeddable, a @JmixEmbedded value object, or any @JmixEntity with no id at all. Such an instance cannot be the subject of its own payload entry: there is no id to encode it by, or to look it up again on restore.
    • createEntry

      protected com.google.gson.JsonObject createEntry(DataContext dataContext, Object entity)
      Builds one element of the payload's "entities" array. A not-yet-persisted instance and an instance with no tracked attribute changes are stored whole: the first because values set before it entered the data context were never tracked, the second because there is nothing to diff.
    • addDiffMembers

      protected void addDiffMembers(com.google.gson.JsonObject entry, Object entity, Set<String> attributes)
      Adds the "attributes" member of a diff entry, by serializing the instance and pruning the result down to the given attribute names, which keeps EntitySerialization's handling of dates, enums, embedded objects and references. SERIALIZE_NULLS is mandatory: without it Gson drops null members, so an attribute the user cleared would carry no value and restoring it would leave the old one in place.
    • isToMany

      protected boolean isToMany(MetaClass metaClass, String attribute)
      A dotted path never denotes a to-many attribute: the data context tracks embedded changes only for scalar sub-attributes.
    • collectionIds

      protected @Nullable com.google.gson.JsonArray collectionIds(Object entity, String attribute)
      The ids of a to-many attribute's current elements, in their current order. Elements are stored as ids rather than nested: an element the user also edited already has its own entry.

      Returns null when there is nothing to store ids from - the attribute is not loaded, or its value is not a collection. Both drop a change the tracker did report, so both are logged.

    • copyAttributeMember

      protected void copyAttributeMember(com.google.gson.JsonObject serialized, com.google.gson.JsonObject target, String attribute)
      Copies one attribute from a serialized instance into the diff, following a dotted path into nested objects so that an embedded attribute keeps only the sub-attribute the user changed. A JSON null is copied like any other value: the user cleared the attribute, and leaving the member out would restore the old value instead. An attribute the serializer did not emit at all (it is not loaded on the instance) cannot be recorded, and is logged.
    • applyPayload

      public List<Object> applyPayload(String payload, DataContext dataContext)
      Applies a draft payload onto the given data context: merges the drafted "entities" instances (keeping the version already loaded into the context, if any), marks them modified, and re-removes the drafted "removed" instances.
      Parameters:
      payload - JSON payload produced by createPayload(DataContext)
      dataContext - data context to apply the payload to
      Returns:
      the merged (managed) instances corresponding to the payload's "entities" entries
      Throws:
      DraftFormatException - if the payload is unparseable or has an incompatible format version
    • applyFullEntry

      protected void applyFullEntry(com.google.gson.JsonObject entry, DataContext dataContext, List<Object> applied)
      Merges a whole serialized instance into the context. The "new" flag is corrected from the entry before merging, because DataContext.merge() copies the whole system state onto an already managed instance: a genuinely-loaded instance merged from a new-flagged drafted one would be saved with an INSERT that collides with the existing row's primary key.
    • substituteManagedReferences

      protected void substituteManagedReferences(Object root, DataContext dataContext)
      Replaces every reference reachable from root that the data context already manages with the managed instance itself, so that merging the entry does not deep-merge a drafted copy over it.

      EntitySerialization writes a not-yet-persisted instance in full depth, so a full entry for a new composition element carries whole nested copies of its parent and of everything else reachable. Merging those reverts what another user changed since capture, brings the parent's capture-time version back, and - because full entries are applied first - installs the parent's own drafted values before its diff entry runs, which then registers no change and leaves the parent unsaved. DataContextImpl.internalMerge skips merging entirely when the source instance is the managed one, so substituting is enough to stop all three.

      A reference the context does not manage keeps its drafted copy, which is what carries an instance the reopened view never loaded back into the context.

    • substituteManagedReferences

      protected void substituteManagedReferences(Object instance, DataContext dataContext, Map<Object,Boolean> visited)
    • substituteInCollection

      protected void substituteInCollection(Collection<?> collection, DataContext dataContext, Map<Object,Boolean> visited)
    • findManaged

      protected @Nullable Object findManaged(Object drafted, DataContext dataContext)
      Returns:
      the instance the data context manages for drafted — which may be drafted itself — or null when the context manages none, or when drafted has no id to look it up by
    • applyDiffEntry

      protected void applyDiffEntry(com.google.gson.JsonObject entry, DataContext dataContext, List<Object> applied)
      Replays the drafted attribute changes onto the managed instance. Nothing is force-marked as modified: setting the values goes through the normal setters, so the data context's own change listener registers exactly the attributes whose value actually differs.
    • applyDiffAttributes

      protected void applyDiffAttributes(com.google.gson.JsonObject entry, DataContext dataContext, Object managed)
    • applyDiffCollections

      protected void applyDiffCollections(com.google.gson.JsonObject entry, DataContext dataContext, Object managed)
      Restores a to-many attribute's membership by mutating the managed collection in place. Replacing the collection instead would restore the membership without recording the attribute as modified.

      An attribute that is no longer in the metadata is skipped like any other unknown drafted attribute, rather than letting MetaClass.getProperty throw and discard the whole draft.

    • collectKnownAttributes

      protected List<String> collectKnownAttributes(MetaClass metaClass, com.google.gson.JsonObject attributes, com.google.gson.JsonObject target)
      Copies the members that still exist in the metadata into target and returns their dotted paths. Embedded members are walked into, so that {"address":{"city":..}} yields "address.city"; every other member is a leaf. Filtering has to happen before the sparse instance is deserialized, so that an attribute dropped by an application upgrade is skipped rather than failing the whole draft inside EntitySerialization.
    • applyAttribute

      protected void applyAttribute(DataContext dataContext, Object managed, Object sparse, String path)
      Copies one decoded attribute value onto the managed instance. A to-one reference arrives as a whole copy of the referenced instance as it was loaded at capture time, not as a bare id: prefer the instance the context already manages, and only when the context manages nothing for that id merge the copy as an already-persisted instance - which installs that copy's loaded attributes into the context. A reference to an instance that was itself new at draft time is found by the lookup, because new instances are stored as full entries and were merged before any diff entry is applied.
    • ensureEmbeddedExists

      protected void ensureEmbeddedExists(Object managed, MetaPropertyPath propertyPath)
      Creates the intermediate embedded objects a dotted path needs, when the managed instance has none. Without this, restoring "address.city" onto an instance whose address is null fails.
    • mode

      protected String mode(com.google.gson.JsonObject entry)
      The entry's mode, validated: a payload with a missing or unrecognized mode is a format error, not something to guess at.
    • entryId

      protected Id<?> entryId(com.google.gson.JsonObject entry)
      The entry's target id, validated: a missing or non-string "id" member is a format error rather than a raw Gson exception.
    • isNewEntry

      protected boolean isNewEntry(com.google.gson.JsonObject entry)
    • versionDiffers

      public boolean versionDiffers(String payload, DataContext dataContext)
      Checks whether any drafted instance's version differs from the version of the instance already loaded into the given data context, meaning the persisted data changed since the draft was captured.
      Parameters:
      payload - JSON payload produced by createPayload(DataContext)
      dataContext - data context to check against
      Returns:
      true if a version conflict is detected
    • fullEntryVersionDiffers

      protected boolean fullEntryVersionDiffers(com.google.gson.JsonObject entry, DataContext dataContext)
    • diffEntryVersionDiffers

      protected boolean diffEntryVersionDiffers(com.google.gson.JsonObject entry, DataContext dataContext)
    • currentVersion

      protected @Nullable Object currentVersion(DataContext dataContext, Object drafted)
    • parse

      protected com.google.gson.JsonObject parse(String payload)
      Parses and structurally validates a draft payload: the root must be a JSON object with a numeric "formatVersion" member matching FORMAT_VERSION, and array "entities"/"removed" members. Any deviation - unparseable JSON, a non-object root, missing/malformed members, or an incompatible format version - is reported as DraftFormatException, never as a raw Gson/JDK exception.
    • requireArrayMember

      protected void requireArrayMember(com.google.gson.JsonObject root, String memberName)
      Throws DraftFormatException unless the given member of the root object is present and is a JSON array - guards root.getAsJsonArray(memberName) call sites in applyPayload(java.lang.String, io.jmix.flowui.model.DataContext) and versionDiffers(java.lang.String, io.jmix.flowui.model.DataContext) against a raw NullPointerException/IllegalStateException for a structurally incomplete payload.
    • requireArray

      protected com.google.gson.JsonArray requireArray(com.google.gson.JsonElement element, String memberName)
      The element as a JSON array, or DraftFormatException - guards a getAsJsonArray() call site against a raw IllegalStateException for a malformed payload.
    • requireString

      protected String requireString(com.google.gson.JsonElement element, String memberName)
      The element as a string, or DraftFormatException - guards a getAsString() call site against a raw IllegalStateException/UnsupportedOperationException for a malformed payload.