content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *        http://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   */
  package org.apache.causeway.extensions.executionlog.applib.dom;
  
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.math.BigDecimal;
  import java.time.format.DateTimeFormatter;
  import java.util.Arrays;
  import java.util.List;
  import java.util.UUID;
  
  import jakarta.annotation.Priority;
  import jakarta.inject.Inject;
  import jakarta.inject.Named;
  import jakarta.validation.constraints.Digits;
  
  import org.springframework.stereotype.Service;
  
  import org.apache.causeway.applib.annotation.DomainObject;
  import org.apache.causeway.applib.annotation.DomainObjectLayout;
  import org.apache.causeway.applib.annotation.Editing;
  import org.apache.causeway.applib.annotation.ObjectSupport;
  import org.apache.causeway.applib.annotation.Optionality;
  import org.apache.causeway.applib.annotation.Parameter;
  import org.apache.causeway.applib.annotation.PriorityPrecedence;
  import org.apache.causeway.applib.annotation.Programmatic;
  import org.apache.causeway.applib.annotation.Property;
  import org.apache.causeway.applib.annotation.Publishing;
  import org.apache.causeway.applib.annotation.ValueSemantics;
  import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
  import org.apache.causeway.applib.mixins.system.HasInteractionId;
  import org.apache.causeway.applib.mixins.system.HasInteractionIdAndSequence;
  import org.apache.causeway.applib.services.bookmark.Bookmark;
  import org.apache.causeway.applib.services.bookmark.BookmarkService;
  import org.apache.causeway.applib.services.iactn.ActionInvocation;
  import org.apache.causeway.applib.services.iactn.Execution;
  import org.apache.causeway.applib.services.iactn.HasInteractionDto;
  import org.apache.causeway.applib.services.iactn.PropertyEdit;
  import org.apache.causeway.applib.services.tablecol.TableColumnOrderForCollectionTypeAbstract;
  import org.apache.causeway.applib.util.ObjectContracts;
  import org.apache.causeway.applib.util.TitleBuffer;
  import org.apache.causeway.applib.util.ToString;
  import org.apache.causeway.commons.internal.base._Temporals;
  import org.apache.causeway.extensions.executionlog.applib.CausewayModuleExtExecutionLogApplib;
  import org.apache.causeway.schema.ixn.v2.InteractionDto;
  import org.apache.causeway.schema.ixn.v2.MemberExecutionDto;
  
  import lombok.NoArgsConstructor;
  import org.jspecify.annotations.NonNull;
  import lombok.experimental.UtilityClass;
  
  /**
   * A persistent representation of an {@link org.apache.causeway.applib.services.iactn.Execution execution} (property edit or
   * action invocation), within a wider {@link org.apache.causeway.applib.services.iactn.Interaction interaction}.
   *
   *  Note that this class implements {@link HasInteractionDto}, providing access to
   *  {@link org.apache.causeway.applib.services.iactn.Interaction}, a serialized representation containing the {@link org.apache.causeway.applib.services.iactn.Execution}.
   *
   * @since 2.0 {@index}
   */
  @Named(ExecutionLogEntry.LOGICAL_TYPE_NAME)
  @DomainObject(
          editing = Editing.DISABLED,
          entityChangePublishing = Publishing.DISABLED
  )
  @DomainObjectLayout(
          titleUiEvent = ExecutionLogEntry.TitleUiEvent.class,
          iconUiEvent = ExecutionLogEntry.IconUiEvent.class,
          cssClassUiEvent = ExecutionLogEntry.CssClassUiEvent.class,
          layoutUiEvent = ExecutionLogEntry.LayoutUiEvent.class
  )
  @NoArgsConstructor
  public abstract class ExecutionLogEntry
  implements Comparable<ExecutionLogEntry>, DomainChangeRecord, HasInteractionIdAndSequence, HasInteractionDto {
  
      public final static String LOGICAL_TYPE_NAME = CausewayModuleExtExecutionLogApplib.NAMESPACE + ".ExecutionLogEntry";
      public static final String SCHEMA = CausewayModuleExtExecutionLogApplib.SCHEMA;
      public static final String TABLE = "ExecutionLogEntry";
  
      public static class TitleUiEvent extends CausewayModuleExtExecutionLogApplib.TitleUiEvent<ExecutionLogEntry> { }
      public static class IconUiEvent extends CausewayModuleExtExecutionLogApplib.IconUiEvent<ExecutionLogEntry> { }
      public static class CssClassUiEvent extends CausewayModuleExtExecutionLogApplib.CssClassUiEvent<ExecutionLogEntry> { }
      public static class LayoutUiEvent extends CausewayModuleExtExecutionLogApplib.LayoutUiEvent<ExecutionLogEntry> { }
  
      public static abstract class PropertyDomainEvent<T> extends CausewayModuleExtExecutionLogApplib.PropertyDomainEvent<ExecutionLogEntry, T> { }
      public static abstract class CollectionDomainEvent<T> extends CausewayModuleExtExecutionLogApplib.CollectionDomainEvent<ExecutionLogEntry, T> { }
      public static abstract class ActionDomainEvent extends CausewayModuleExtExecutionLogApplib.ActionDomainEvent<ExecutionLogEntry> { }
  
      @UtilityClass
      public static class Nq {
          public static final String FIND_BY_INTERACTION_ID = LOGICAL_TYPE_NAME + ".findByInteractionId";
          public static final String FIND_BY_INTERACTION_ID_AND_SEQUENCE = LOGICAL_TYPE_NAME + ".findByInteractionIdAndSequence";
          public static final String FIND_BY_TARGET_AND_TIMESTAMP_BETWEEN = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampBetween";
          public static final String FIND_BY_TARGET_AND_TIMESTAMP_AFTER = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampAfter";
          public static final String FIND_BY_TARGET_AND_TIMESTAMP_BEFORE = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampBefore";
          public static final String FIND_BY_TARGET = LOGICAL_TYPE_NAME + ".findByTarget";
          public static final String FIND_BY_TIMESTAMP_BETWEEN = LOGICAL_TYPE_NAME + ".findByTimestampBetween";
          public static final String FIND_BY_TIMESTAMP_AFTER = LOGICAL_TYPE_NAME + ".findByTimestampAfter";
          public static final String FIND_BY_TIMESTAMP_BEFORE = LOGICAL_TYPE_NAME + ".findByTimestampBefore";
          public static final String FIND = LOGICAL_TYPE_NAME + ".find";
          public static final String FIND_MOST_RECENT = LOGICAL_TYPE_NAME + ".findMostRecent";
          public static final String FIND_RECENT_BY_USERNAME = LOGICAL_TYPE_NAME + ".findRecentByUsername";
          public static final String FIND_RECENT_BY_TARGET = LOGICAL_TYPE_NAME + ".findRecentByTarget";
      }
  
      @UtilityClass
      protected static class Util {
          public static String abbreviated(final String str, final int maxLength) {
              return str != null ? (str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "...") : null;
          }
      }
  
      @Inject BookmarkService bookmarkService;
  
      public ExecutionLogEntry(final @NonNull Execution<? extends MemberExecutionDto,?> execution) {
          init(execution);
      }
  
      @Programmatic
      public void init(final Execution<? extends MemberExecutionDto, ?> execution) {
          var interactionId = execution.getInteraction().getInteractionId();
          setInteractionId(interactionId);
  
          var memberExecutionDto = execution.getDto();
          setSequence(memberExecutionDto.getSequence());
  
          var interactionDto = new InteractionDto();
          interactionDto.setInteractionId(interactionId.toString());
          interactionDto.setExecution(memberExecutionDto);
          setInteractionDto(interactionDto);
  
          setTimestamp(execution.getStartedAt());
          setStartedAt(execution.getStartedAt());
          setCompletedAt(execution.getCompletedAt());
  
          setLogicalMemberIdentifier(memberExecutionDto.getLogicalMemberIdentifier());
  
          setTarget(Bookmark.forOidDto(memberExecutionDto.getTarget()));
          setUsername(memberExecutionDto.getUsername());
  
          if(execution instanceof PropertyEdit) {
              setExecutionType(ExecutionLogEntryType.PROPERTY_EDIT);
          } else if(execution instanceof ActionInvocation) {
              setExecutionType(ExecutionLogEntryType.ACTION_INVOCATION);
          } else {
              // shouldn't happen, there are no other subtypes
              throw new IllegalArgumentException(String.format("Execution subtype unknown: %s", execution.getClass().getName()));
          }
      }
  
      private static final DateTimeFormatter formatter =
              DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
  
      @ObjectSupport public String title() {
          return new TitleBuffer()
                  .append(formatter.format(getTimestamp().toLocalDateTime()))
                  .append(" ")
                  .append(getLogicalMemberIdentifier())
                  .toString();
      }
  
      @DomainChangeRecord.Type
      @Override
      public ChangeType getType() {
          return ChangeType.EXECUTION;
      }
  
      /**
       * The unique identifier (a GUID) of the {@link org.apache.causeway.applib.services.iactn.Interaction} in which this execution occurred.
       *
       * <p>
       * The combination of ({@link #getInteractionId() interactionId}, {@link #getSequence() sequence}) makes up the
       * primary key.
       * </p>
       */
      @Property(
              domainEvent = InteractionId.DomainEvent.class
      )
      @DomainChangeRecord.InteractionId
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface InteractionId {
          class DomainEvent extends PropertyDomainEvent<UUID> {}
          int MAX_LENGTH = HasInteractionId.InteractionId.MAX_LENGTH;
          String NAME = "interactionId";
          boolean NULLABLE = HasInteractionId.InteractionId.NULLABLE;
          String ALLOWS_NULL = HasInteractionId.InteractionId.ALLOWS_NULL;
      }
      @Override
      @InteractionId
      public abstract UUID getInteractionId();
      public abstract void setInteractionId(UUID interactionId);
  
      /**
       * The 0-based additional identifier of an execution event within the given {@link #getInteractionId() interaction}.
       *
       * <p>
       * The combination of ({@link #getInteractionId() interactionId}, {@link #getSequence() sequence}) makes up the
       * primary key.
       * </p>
       */
      @Property(
              domainEvent = Sequence.DomainEvent.class
      )
      @HasInteractionIdAndSequence.Sequence
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Sequence {
          class DomainEvent extends PropertyDomainEvent<Integer> {}
          String NAME = "sequence";
          boolean NULLABLE = HasInteractionIdAndSequence.Sequence.NULLABLE;
          String ALLOWS_NULL = HasInteractionIdAndSequence.Sequence.ALLOWS_NULL;
      }
      @Sequence
      @Override
      public abstract int getSequence();
      public abstract void setSequence(int sequence);
  
      @Property(
              domainEvent = ExecutionType.DomainEvent.class
      )
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface ExecutionType {
          class DomainEvent extends PropertyDomainEvent<ExecutionLogEntryType> {}
          boolean NULLABLE = false;
          String ALLOWS_NULL = "false";
          int MAX_LENGTH = 30;
      }
      @ExecutionType
      public abstract ExecutionLogEntryType getExecutionType();
      public abstract void setExecutionType(ExecutionLogEntryType executionType);
  
      @Property(
              domainEvent = Username.DomainEvent.class
      )
      @DomainChangeRecord.Username
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Username {
          class DomainEvent extends PropertyDomainEvent<String> {}
          int MAX_LENGTH = DomainChangeRecord.Username.MAX_LENGTH;
          boolean NULLABLE = DomainChangeRecord.Username.NULLABLE;
          String ALLOWS_NULL = DomainChangeRecord.Username.ALLOWS_NULL;
      }
      @Override
      @Username
      public abstract String getUsername();
      public abstract void setUsername(String userName);
  
      @Property(
              domainEvent = Timestamp.DomainEvent.class
      )
      @DomainChangeRecord.Timestamp
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Timestamp {
          class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
          boolean NULLABLE = DomainChangeRecord.Timestamp.NULLABLE;
          String ALLOWS_NULL = DomainChangeRecord.Timestamp.ALLOWS_NULL;
      }
      @Timestamp
      @Override
      public abstract java.sql.Timestamp getTimestamp();
      public abstract void setTimestamp(java.sql.Timestamp timestamp);
  
      @Property(
              domainEvent = Target.DomainEvent.class
      )
      @DomainChangeRecord.Target
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Target {
          class DomainEvent extends PropertyDomainEvent<Bookmark> {}
          int MAX_LENGTH = DomainChangeRecord.Target.MAX_LENGTH;
          boolean NULLABLE = DomainChangeRecord.Target.NULLABLE;
          String ALLOWS_NULL = DomainChangeRecord.Target.ALLOWS_NULL;
      }
      @Override
      @Target
      public abstract Bookmark getTarget();
      public abstract void setTarget(Bookmark target);
  
      /**
       * String representation of the invoked action or edited property.
       *
       * <p>
       * This is the <i>logical</i> member identifier because it does not matter whether the action/property is declared
       * on the type or is contributed.
       *
       */
      @Property(
              domainEvent = LogicalMemberIdentifier.DomainEvent.class,
              editing = Editing.DISABLED
      )
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface LogicalMemberIdentifier {
          class DomainEvent extends PropertyDomainEvent<String> {}
          int MAX_LENGTH = 255;
          boolean NULLABLE = false;
          String ALLOWS_NULL = "false";
      }
      @Override
      @LogicalMemberIdentifier
      public abstract String getLogicalMemberIdentifier();
      public abstract void setLogicalMemberIdentifier(String logicalMemberIdentifier);
  
      @Property(
              domainEvent = InteractionDtoAnnot.DomainEvent.class
      )
      @HasInteractionDto.InteractionDtoAnnot
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface InteractionDtoAnnot {
          class DomainEvent extends PropertyDomainEvent<InteractionDto> {}
          boolean NULLABLE = HasInteractionDto.InteractionDtoAnnot.NULLABLE;
          String ALLOWS_NULL = HasInteractionDto.InteractionDtoAnnot.ALLOWS_NULL;
      }
      @ValueSemantics(provider = "pretty-render") // attempting to fix
      @InteractionDtoAnnot
      @Override
      public abstract InteractionDto getInteractionDto();
      public abstract void setInteractionDto(InteractionDto commandDto);
  
      @Property(
              domainEvent = StartedAt.DomainEvent.class,
              editing = Editing.DISABLED,
              optionality = Optionality.OPTIONAL
      )
      @Parameter(
              optionality = Optionality.OPTIONAL
      )
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface StartedAt {
          class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
          boolean NULLABLE = true;
          String ALLOWS_NULL = "true";
      }
      @StartedAt
      public abstract java.sql.Timestamp getStartedAt();
      public abstract void setStartedAt(java.sql.Timestamp startedAt);
  
      @Property(
              domainEvent = CompletedAt.DomainEvent.class,
              editing = Editing.DISABLED,
              optionality = Optionality.OPTIONAL
      )
      @Parameter(
              optionality = Optionality.OPTIONAL
      )
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface CompletedAt {
          class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
          boolean NULLABLE = true;
          String ALLOWS_NULL = "true";
      }
      @CompletedAt
      public abstract java.sql.Timestamp getCompletedAt();
      public abstract void setCompletedAt(java.sql.Timestamp completedAt);
  
      @Property(
              domainEvent = Duration.DomainEvent.class,
              editing = Editing.DISABLED
      )
      @Digits(integer=Duration.DIGITS_INTEGER, fraction=Duration.DIGITS_FRACTION)
      @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Duration {
          class DomainEvent extends PropertyDomainEvent<BigDecimal> {}
          int DIGITS_INTEGER = 5;
          int DIGITS_FRACTION = 3;
      }
      /**
       * The number of seconds (to 3 decimal places) that this execution lasted, derived from
       * {@link #getStartedAt()} and {@link #getCompletedAt()}.
       */
      @Duration
      public BigDecimal getDuration() {
          return _Temporals.secondsBetweenAsDecimal(getStartedAt(), getCompletedAt())
                  .orElse(null);
      }
  
      @Override
      public int compareTo(final ExecutionLogEntry other) {
          return this.getTimestamp().compareTo(other.getTimestamp());
      }
  
      static final ToString<ExecutionLogEntry> stringifier = ObjectContracts
              .toString("interactionId", ExecutionLogEntry::getInteractionId)
              .thenToString("sequence", ExecutionLogEntry::getSequence)
              .thenToString("username", ExecutionLogEntry::getUsername)
              .thenToString("type", ExecutionLogEntry::getExecutionType)
              .thenToString("timestamp", ExecutionLogEntry::getTimestamp)
              .thenToString("target", ExecutionLogEntry::getTarget)
              .thenToString("logicalMemberIdentifier", ExecutionLogEntry::getLogicalMemberIdentifier)
              .thenToStringOmitIfAbsent("startedAt", ExecutionLogEntry::getStartedAt)
              .thenToStringOmitIfAbsent("completedAt", ExecutionLogEntry::getCompletedAt);
  
      @Override
      public String toString() {
          return stringifier.toString(this);
      }
  
      @Service
      @Priority(PriorityPrecedence.LATE - 10) // before the framework's own default.
      public static class TableColumnOrderDefault extends TableColumnOrderForCollectionTypeAbstract<ExecutionLogEntry> {
  
          public TableColumnOrderDefault() { super(ExecutionLogEntry.class); }
  
          @Override
          protected List<String> orderParented(final Object parent, final String collectionId, final List<String> propertyIds) {
              return ordered(propertyIds);
          }
  
          @Override
          protected List<String> orderStandalone(final List<String> propertyIds) {
              return ordered(propertyIds);
          }
  
          private List<String> ordered(final List<String> propertyIds) {
              return Arrays.asList(
                      "timestamp", "target", "logicalMemberIdentifier", "username", "duration", "interactionId", "sequence"
              );
          }
      }
  
  }
  