This is an automated email from the ASF dual-hosted git repository.

sammichen pushed a commit to branch HDDS-8342
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-8342 by this push:
     new 2b5f88aaf0 HDDS-12808. Implement OM entities of lifecycle 
configurations (#8275)
2b5f88aaf0 is described below

commit 2b5f88aaf0bac1cfa7ed42334f6ae1c57f03a927
Author: XiChen <[email protected]>
AuthorDate: Mon Apr 28 12:52:46 2025 +0800

    HDDS-12808. Implement OM entities of lifecycle configurations (#8275)
    
    Co-contributed by Mohanad Elsafty ([email protected])
---
 .../apache/hadoop/ozone/om/helpers/OmLCAction.java |  51 +++++
 .../hadoop/ozone/om/helpers/OmLCExpiration.java    | 158 ++++++++++++++
 .../apache/hadoop/ozone/om/helpers/OmLCFilter.java | 131 ++++++++++++
 .../apache/hadoop/ozone/om/helpers/OmLCRule.java   | 232 +++++++++++++++++++++
 .../ozone/om/helpers/OmLifecycleConfiguration.java | 218 +++++++++++++++++++
 .../om/helpers/OmLifecycleRuleAndOperator.java     | 128 ++++++++++++
 .../apache/hadoop/ozone/om/helpers/OMLCUtils.java  | 125 +++++++++++
 .../ozone/om/helpers/TestOmLCExpiration.java       | 167 +++++++++++++++
 .../hadoop/ozone/om/helpers/TestOmLCFilter.java    |  84 ++++++++
 .../hadoop/ozone/om/helpers/TestOmLCRule.java      | 171 +++++++++++++++
 .../om/helpers/TestOmLifeCycleConfiguration.java   | 191 +++++++++++++++++
 .../om/helpers/TestOmLifecycleRuleAndOperator.java |  70 +++++++
 12 files changed, 1726 insertions(+)

diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java
new file mode 100644
index 0000000000..d466a11218
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCAction.java
@@ -0,0 +1,51 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * Interface that encapsulates lifecycle rule actions.
+ * This class serves as a foundation for various action types in lifecycle
+ * configuration, such as Expiration and (in the future) Transition.
+ */
+public interface OmLCAction {
+
+  /**
+   * Validates the action configuration.
+   * Each concrete action implementation must define its own validation logic.
+   *
+   * @throws OMException if the validation fails
+   */
+  void valid() throws OMException;
+
+  /**
+   * Returns the action type.
+   *
+   * @return the type of this action
+   */
+  ActionType getActionType();
+
+  /**
+   * Enum defining supported action types.
+   */
+  enum ActionType {
+    EXPIRATION,
+    // Future action types can be added here (e.g., TRANSITION)
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java
new file mode 100644
index 0000000000..06d39acd2d
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCExpiration.java
@@ -0,0 +1,158 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import net.jcip.annotations.Immutable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * A class that encapsulates lifecycle rule expiration action.
+ * This class extends OmLCAction and represents the expiration
+ * action type in lifecycle configuration.
+ */
+@Immutable
+public final class OmLCExpiration implements OmLCAction {
+  private final Integer days;
+  private final String date;
+
+  private OmLCExpiration() {
+    throw new UnsupportedOperationException("Default constructor is not 
supported. Use Builder.");
+  }
+
+  private OmLCExpiration(Builder builder) {
+    this.days = builder.days;
+    this.date = builder.date;
+  }
+
+  public Integer getDays() {
+    return days;
+  }
+
+  public String getDate() {
+    return date;
+  }
+
+  @Override
+  public ActionType getActionType() {
+    return ActionType.EXPIRATION;
+  }
+
+  /**
+   * Validates the expiration configuration.
+   * - Days must be a positive number greater than zero if set
+   * - Either days or date should be specified, but not both or neither
+   * - The date value must conform to the ISO 8601 format
+   * - The date value must be in the future
+   * - The date value must be at midnight UTC (00:00:00Z)
+   *
+   * @throws OMException if the validation fails
+   */
+  @Override
+  public void valid() throws OMException {
+    boolean hasDays = days != null;
+    boolean hasDate = !StringUtils.isBlank(date);
+
+    if (hasDays == hasDate) {
+      throw new OMException("Invalid lifecycle configuration: Either 'days' or 
'date' " +
+          "should be specified, but not both or neither.", 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+    if (hasDays) {
+      if (days <= 0) {
+        throw new OMException("'Days' for Expiration action must be a positive 
integer greater than zero.",
+            OMException.ResultCodes.INVALID_REQUEST);
+      }
+    }
+    if (hasDate) {
+      validateExpirationDate(date);
+    }
+  }
+
+  /**
+   * Validates that the expiration date is:
+   * - In the ISO 8601 format
+   * - Includes both time and time zone (neither can be omitted)
+   * - In the future
+   * - Represents midnight UTC (00:00:00Z) when converted to UTC.
+   *
+   * @param expirationDate The date string to validate
+   * @throws OMException if the date is invalid
+   */
+  private void validateExpirationDate(String expirationDate) throws 
OMException {
+    try {
+      ZonedDateTime parsedDate = ZonedDateTime.parse(expirationDate, 
DateTimeFormatter.ISO_DATE_TIME);
+      ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
+      // Convert to UTC for validation
+      ZonedDateTime dateInUTC = parsedDate.withZoneSameInstant(ZoneOffset.UTC);
+      // The date value must conform to the ISO 8601 format, be in the future.
+      if (dateInUTC.isBefore(now)) {
+        throw new OMException("Invalid lifecycle configuration: 'Date' must be 
in the future",
+            OMException.ResultCodes.INVALID_REQUEST);
+      }
+      // Verify that the time is midnight UTC (00:00:00Z)
+      if (dateInUTC.getHour() != 0 ||
+          dateInUTC.getMinute() != 0 ||
+          dateInUTC.getSecond() != 0 ||
+          dateInUTC.getNano() != 0) {
+        throw new OMException("Invalid lifecycle configuration: 'Date' must 
represent midnight UTC (00:00:00Z). " +
+            "Examples: '2042-04-02T00:00:00Z' or '2042-04-02T00:00:00+00:00'",
+            OMException.ResultCodes.INVALID_REQUEST);
+      }
+    } catch (DateTimeParseException ex) {
+      throw new OMException("Invalid lifecycle configuration: 'Date' must be 
in ISO 8601 format with " +
+          "time and time zone included. Examples: '2042-04-02T00:00:00Z' or 
'2042-04-02T00:00:00+00:00'",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "OmLCExpiration{" +
+        "days=" + days +
+        ", date='" + date + '\'' +
+        '}';
+  }
+
+  /**
+   * Builder of OmLCExpiration.
+   */
+  public static class Builder {
+    private Integer days = null;
+    private String date = null;
+
+    public Builder setDays(int lcDays) {
+      this.days = lcDays;
+      return this;
+    }
+
+    public Builder setDate(String lcDate) {
+      this.date = lcDate;
+      return this;
+    }
+
+    public OmLCExpiration build() throws OMException {
+      OmLCExpiration omLCExpiration = new OmLCExpiration(this);
+      omLCExpiration.valid();
+      return omLCExpiration;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java
new file mode 100644
index 0000000000..8ef0df6bbd
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCFilter.java
@@ -0,0 +1,131 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import jakarta.annotation.Nullable;
+import net.jcip.annotations.Immutable;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * A class that encapsulates lifecycle rule filter.
+ * At the moment only prefix is supported in filter.
+ */
+@Immutable
+public final class OmLCFilter {
+
+  private final String prefix;
+  private final String tagKey;
+  private final String tagValue;
+  private final OmLifecycleRuleAndOperator andOperator;
+
+  private OmLCFilter() {
+    throw new UnsupportedOperationException("Default constructor is not 
supported. Use Builder.");
+  }
+
+  private OmLCFilter(Builder builder) {
+    this.prefix = builder.prefix;
+    this.andOperator = builder.andOperator;
+    this.tagKey = builder.tagKey;
+    this.tagValue = builder.tagValue;
+  }
+
+  /**
+   * Validates the OmLCFilter.
+   * Ensures that only one of prefix, tag, or andOperator is set.
+   * You can specify an empty filter, in which case the rule applies to all 
objects in the bucket.
+   * Prefix can be "", in which case the rule applies to all objects in the 
bucket.
+   * Ref: <a 
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-filters.html#filter-examples";>...</a>
+   * If the validation fails, an OMException is thrown.
+   *
+   * @throws OMException if the filter is invalid.
+   */
+  public void valid() throws OMException {
+    boolean hasPrefix = prefix != null;
+    boolean hasTag = tagKey != null && tagValue != null;
+    boolean hasAndOperator = andOperator != null;
+
+    if ((hasPrefix && (hasTag || hasAndOperator)) || (hasTag && 
hasAndOperator)) {
+      throw new OMException("Invalid lifecycle filter configuration: Only one 
of 'Prefix'," +
+          " 'Tag', or 'AndOperator' should be specified.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (andOperator != null) {
+      andOperator.valid();
+    }
+  }
+
+  public OmLifecycleRuleAndOperator getAndOperator() {
+    return andOperator;
+  }
+
+  @Nullable
+  public String getPrefix() {
+    return prefix;
+  }
+
+  @Nullable
+  public Pair<String, String> getTag() {
+    return Pair.of(tagKey, tagValue);
+  }
+
+  @Override
+  public String toString() {
+    return "OmLCFilter{" +
+        "prefix='" + prefix + '\'' +
+        ", tagKey='" + tagKey + '\'' +
+        ", tagValue='" + tagValue + '\'' +
+        ", andOperator=" + andOperator +
+        '}';
+  }
+
+  /**
+   * Builder of OmLCFilter.
+   */
+  public static class Builder {
+    private String prefix = null;
+    private String tagKey = null;
+    private String tagValue = null;
+    private OmLifecycleRuleAndOperator andOperator = null;
+
+    public Builder setPrefix(String lcPrefix) {
+      this.prefix = lcPrefix;
+      return this;
+    }
+
+    public Builder setTag(String key, String value) {
+      this.tagKey = key;
+      this.tagValue = value;
+      return this;
+    }
+
+    public Builder setAndOperator(OmLifecycleRuleAndOperator andOp) {
+      this.andOperator = andOp;
+      return this;
+    }
+
+    public OmLCFilter build() throws OMException {
+      OmLCFilter omLCFilter = new OmLCFilter(this);
+      omLCFilter.valid();
+      return omLCFilter;
+    }
+  }
+
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java
new file mode 100644
index 0000000000..96f8769f39
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLCRule.java
@@ -0,0 +1,232 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import net.jcip.annotations.Immutable;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * A class that encapsulates lifecycle rule.
+ */
+@Immutable
+public final class OmLCRule {
+
+  public static final int LC_ID_LENGTH = 48;
+  // Ref: 
https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-rules.html#intro-lifecycle-rule-id
+  public static final int LC_ID_MAX_LENGTH = 255;
+
+  private final String id;
+  private final String prefix;
+  private final boolean enabled;
+  // List of actions for this rule
+  private final List<OmLCAction> actions;
+  private final OmLCFilter filter;
+
+  private final boolean isPrefixEnable;
+  private final boolean isTagEnable;
+
+  private OmLCRule() {
+    throw new UnsupportedOperationException("Default constructor is not 
supported. Use Builder.");
+  }
+
+  private OmLCRule(Builder builder) {
+    this.prefix = builder.prefix;
+    this.enabled = builder.enabled;
+    this.actions = Collections.unmodifiableList(new 
ArrayList<>(builder.actions));
+    this.filter = builder.filter;
+    // If no ID is specified in the lifecycle configure, a random ID will be 
generated
+    if (StringUtils.isEmpty(builder.id)) {
+      this.id = RandomStringUtils.randomAlphanumeric(LC_ID_LENGTH);
+    } else {
+      this.id = builder.id;
+    }
+
+    OmLifecycleRuleAndOperator andOperator = filter != null ? 
filter.getAndOperator() : null;
+
+    this.isPrefixEnable = prefix != null ||
+        (filter != null && filter.getPrefix() != null) ||
+        (andOperator != null && andOperator.getPrefix() != null);
+
+    this.isTagEnable = (filter != null && filter.getTag() != null) ||
+        (andOperator != null && !andOperator.getTags().isEmpty());
+  }
+
+
+  public String getId() {
+    return id;
+  }
+
+  public String getPrefix() {
+    return prefix;
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public List<OmLCAction> getActions() {
+    return actions;
+  }
+
+  /**
+   * Get the expiration action if present.
+   *
+   * @return the expiration action if present, null otherwise
+   */
+  public OmLCExpiration getExpiration() {
+    for (OmLCAction action : actions) {
+      if (action instanceof OmLCExpiration) {
+        return (OmLCExpiration) action;
+      }
+    }
+    return null;
+  }
+
+  public OmLCFilter getFilter() {
+    return filter;
+  }
+
+  public boolean isPrefixEnable() {
+    return isPrefixEnable;
+  }
+
+  public boolean isTagEnable() {
+    return isTagEnable;
+  }
+
+  /**
+   * Validates the lifecycle rule.
+   * - ID length should not exceed the allowed limit
+   * - At least one action must be specified
+   * - Filter and Prefix cannot be used together
+   * - Prefix can be "", in which case the rule applies to all objects in the 
bucket.
+   * - Actions must be valid
+   * - Filter must be valid
+   * - There must be at most one Expiration action per rule
+   *
+   * @throws OMException if the validation fails
+   */
+  public void valid() throws OMException {
+    if (id.length() > LC_ID_MAX_LENGTH) {
+      throw new OMException("ID length should not exceed allowed limit of " + 
LC_ID_MAX_LENGTH,
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (actions == null || actions.isEmpty()) {
+      throw new OMException("At least one action needs to be specified in a 
rule.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Check that there is at most one Expiration action
+    int expirationActionCount = 0;
+    for (OmLCAction action : actions) {
+      if (action.getActionType() == OmLCAction.ActionType.EXPIRATION) {
+        expirationActionCount++;
+      }
+      if (expirationActionCount > 1) {
+        throw new OMException("A rule can have at most one Expiration action.",
+            OMException.ResultCodes.INVALID_REQUEST);
+      }
+      action.valid();
+    }
+
+    if (prefix != null && filter != null) {
+      throw new OMException("Filter and Prefix cannot be used together.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (filter != null) {
+      filter.valid();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "OmLCRule{" +
+        "id='" + id + '\'' +
+        ", prefix='" + prefix + '\'' +
+        ", enabled=" + enabled +
+        ", isPrefixEnable=" + isPrefixEnable +
+        ", isTagEnable=" + isTagEnable +
+        ", actions=" + actions +
+        ", filter=" + filter +
+        '}';
+  }
+
+  /**
+   * Builder of OmLCRule.
+   */
+  public static class Builder {
+    private String id = "";
+    private String prefix;
+    private boolean enabled;
+    private List<OmLCAction> actions = new ArrayList<>();
+    private OmLCFilter filter;
+
+    public Builder setId(String lcId) {
+      this.id = lcId;
+      return this;
+    }
+
+    public Builder setPrefix(String lcPrefix) {
+      this.prefix = lcPrefix;
+      return this;
+    }
+
+    public Builder setEnabled(boolean lcEnabled) {
+      this.enabled = lcEnabled;
+      return this;
+    }
+
+    public Builder setAction(OmLCAction lcAction) {
+      if (lcAction != null) {
+        this.actions = new ArrayList<>();
+        this.actions.add(lcAction);
+      }
+      return this;
+    }
+
+    public Builder setActions(List<OmLCAction> lcAction) {
+      if (lcAction != null) {
+        this.actions = new ArrayList<>();
+        this.actions.addAll(lcAction);
+      }
+      return this;
+    }
+
+    public Builder setFilter(OmLCFilter lcFilter) {
+      this.filter = lcFilter;
+      return this;
+    }
+
+    public OmLCFilter getFilter() {
+      return filter;
+    }
+
+    public OmLCRule build() throws OMException {
+      OmLCRule omLCRule = new OmLCRule(this);
+      omLCRule.valid();
+      return omLCRule;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java
new file mode 100644
index 0000000000..348ed106ff
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleConfiguration.java
@@ -0,0 +1,218 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import net.jcip.annotations.Immutable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.Auditable;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * A class that encapsulates lifecycle configuration.
+ */
+@Immutable
+public final class OmLifecycleConfiguration extends WithObjectID
+    implements Auditable {
+
+  // Ref: 
https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-rules.html#intro-lifecycle-rule-id
+  public static final int LC_MAX_RULES = 1000;
+  private final String volume;
+  private final String bucket;
+  private final long creationTime;
+  private final List<OmLCRule> rules;
+
+  private OmLifecycleConfiguration() {
+    throw new UnsupportedOperationException("Default constructor is not 
supported. Use Builder.");
+  }
+
+  OmLifecycleConfiguration(OmLifecycleConfiguration.Builder builder) {
+    super(builder);
+    this.volume = builder.volume;
+    this.bucket = builder.bucket;
+    this.rules = Collections.unmodifiableList(builder.rules);
+    this.creationTime = builder.creationTime;
+  }
+
+  public List<OmLCRule> getRules() {
+    return rules;
+  }
+
+  public String getBucket() {
+    return bucket;
+  }
+
+  public String getVolume() {
+    return volume;
+  }
+
+  public long getCreationTime() {
+    return creationTime;
+  }
+
+  /**
+   * Validates the lifecycle configuration.
+   * - Volume and Bucket cannot be blank
+   * - At least one rule needs to be specified
+   * - Number of rules should not exceed the allowed limit
+   * - Rules must have unique IDs
+   * - Each rule is validated individually
+   *
+   * @throws OMException if the validation fails
+   */
+  public void valid() throws OMException {
+    if (StringUtils.isBlank(volume)) {
+      throw new OMException("Invalid lifecycle configuration: Volume cannot be 
blank.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (StringUtils.isBlank(bucket)) {
+      throw new OMException("Invalid lifecycle configuration: Bucket cannot be 
blank.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (rules.isEmpty()) {
+      throw new OMException("At least one rules needs to be specified in a 
lifecycle configuration.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (rules.size() > LC_MAX_RULES) {
+      throw new OMException("The number of lifecycle rules must not exceed the 
allowed limit of "
+          + LC_MAX_RULES + " rules", OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (!hasNoDuplicateID()) {
+      throw new OMException("Invalid lifecycle configuration: Duplicate rule 
IDs found.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    for (OmLCRule rule : rules) {
+      rule.valid();
+    }
+  }
+
+  private boolean hasNoDuplicateID() {
+    return rules.size() == rules.stream()
+        .map(OmLCRule::getId)
+        .collect(Collectors.toSet())
+        .size();
+  }
+
+  public Builder toBuilder() {
+    return new Builder()
+        .setVolume(volume)
+        .setBucket(this.bucket)
+        .setCreationTime(this.creationTime)
+        .setRules(this.rules)
+        .setUpdateID(super.getUpdateID())
+        .setObjectID(super.getObjectID());
+  }
+
+  @Override
+  public String toString() {
+    return "OmLifecycleConfiguration{" +
+        "volume='" + volume + '\'' +
+        ", bucket='" + bucket + '\'' +
+        ", creationTime=" + creationTime +
+        ", rulesCount=" + rules.size() +
+        ", objectID=" + getObjectID() +
+        ", updateID=" + getUpdateID() +
+        '}';
+  }
+  /**
+   * Returns formatted key to be used as prevKey when listing lifecycle
+   * configurations.
+   *
+   * @return volume/bucket
+   */
+  public String getFormattedKey() {
+    return volume + "/" + bucket;
+  }
+
+  @Override
+  public Map<String, String> toAuditMap() {
+    Map<String, String> auditMap = new LinkedHashMap<>();
+    auditMap.put(OzoneConsts.VOLUME, this.volume);
+    auditMap.put(OzoneConsts.BUCKET, this.bucket);
+    auditMap.put(OzoneConsts.CREATION_TIME, String.valueOf(this.creationTime));
+
+    return auditMap;
+  }
+
+  /**
+   * Builder of OmLifecycleConfiguration.
+   */
+  public static class Builder extends WithObjectID.Builder {
+    private String volume = "";
+    private String bucket = "";
+    private long creationTime;
+    private List<OmLCRule> rules = new ArrayList<>();
+
+    public Builder() {
+    }
+
+    public Builder setVolume(String volumeName) {
+      this.volume = volumeName;
+      return this;
+    }
+
+    public Builder setBucket(String bucketName) {
+      this.bucket = bucketName;
+      return this;
+    }
+
+    public Builder setCreationTime(long ctime) {
+      this.creationTime = ctime;
+      return this;
+    }
+
+    public Builder addRule(OmLCRule rule) {
+      this.rules.add(rule);
+      return this;
+    }
+
+    public Builder setRules(List<OmLCRule> lcRules) {
+      this.rules = lcRules;
+      return this;
+    }
+
+    @Override
+    public Builder setObjectID(long oID) {
+      super.setObjectID(oID);
+      return this;
+    }
+
+    @Override
+    public Builder setUpdateID(long uID) {
+      super.setUpdateID(uID);
+      return this;
+    }
+
+    public OmLifecycleConfiguration build() throws OMException {
+      OmLifecycleConfiguration omLifecycleConfiguration = new 
OmLifecycleConfiguration(this);
+      omLifecycleConfiguration.valid();
+      return omLifecycleConfiguration;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java
new file mode 100644
index 0000000000..a8c20e9616
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmLifecycleRuleAndOperator.java
@@ -0,0 +1,128 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import net.jcip.annotations.Immutable;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * A class that encapsulates lifecycleRule andOperator.
+ */
+@Immutable
+public final class OmLifecycleRuleAndOperator {
+
+  private final Map<String, String> tags;
+  private final String prefix;
+
+  private OmLifecycleRuleAndOperator() {
+    throw new UnsupportedOperationException("Default constructor is not 
supported. Use Builder.");
+  }
+
+  private OmLifecycleRuleAndOperator(Builder builder) {
+    this.tags = Collections.unmodifiableMap(new HashMap<>(builder.tags));
+    this.prefix = builder.prefix;
+  }
+
+  @Nonnull
+  public Map<String, String> getTags() {
+    return tags;
+  }
+
+  @Nullable
+  public String getPrefix() {
+    return prefix;
+  }
+
+  /**
+   * Validates the OmLifecycleRuleAndOperator.
+   * Ensures the following:
+   * - Either tags or prefix must be specified.
+   * - If there are tags and no prefix, the tags should be more than one.
+   * - Prefix can be "".
+   * - Prefix alone is not allowed.
+   *
+   * @throws OMException if the validation fails.
+   */
+  public void valid() throws OMException {
+    boolean hasTags = tags != null && !tags.isEmpty();
+    boolean hasPrefix = prefix != null;
+
+    if (!hasTags && !hasPrefix) {
+      throw new OMException("Invalid lifecycle rule andOperator configuration: 
" +
+          "Either 'Tags' or 'Prefix' must be specified.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (hasTags && !hasPrefix && tags.size() == 1) {
+      throw new OMException("Invalid lifecycle rule andOperator configuration: 
" +
+          "If 'Tags' are specified without 'Prefix', there should be more than 
one tag.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    if (hasPrefix && !hasTags) {
+      throw new OMException("Invalid lifecycle rule andOperator configuration: 
" +
+          "'Prefix' alone is not allowed.",
+          OMException.ResultCodes.INVALID_REQUEST);
+    }
+  }
+
+
+  /**
+   * The builder for the OmLifecycleRuleAndOperator class.
+   */
+  public static class Builder {
+    private Map<String, String> tags = new HashMap<>();
+    private String prefix;
+
+    public Builder setPrefix(String lcPrefix) {
+      this.prefix = lcPrefix;
+      return this;
+    }
+
+    public Builder addTag(String key, String value) {
+      this.tags.put(key, value);
+      return this;
+    }
+
+    public Builder setTags(Map<String, String> lcTags) {
+      if (lcTags != null) {
+        this.tags = new HashMap<>(lcTags);
+      }
+      return this;
+    }
+
+    public OmLifecycleRuleAndOperator build() throws OMException {
+      OmLifecycleRuleAndOperator omLifecycleRuleAndOperator = new 
OmLifecycleRuleAndOperator(this);
+      omLifecycleRuleAndOperator.valid();
+      return omLifecycleRuleAndOperator;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "OmLifecycleRuleAndOperator{" +
+        "prefix='" + prefix + '\'' +
+        ", tags=" + tags +
+        '}';
+  }
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/OMLCUtils.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/OMLCUtils.java
new file mode 100644
index 0000000000..566d7ec326
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/OMLCUtils.java
@@ -0,0 +1,125 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.function.Executable;
+
+/**
+ * Util Class for OM lifecycle.
+ */
+public final class OMLCUtils {
+
+  public static final OmLCFilter VALID_OM_LC_FILTER;
+  public static final OmLifecycleRuleAndOperator VALID_OM_LC_AND_OPERATOR;
+
+  static {
+    try {
+      VALID_OM_LC_FILTER = getOmLCFilterBuilder("prefix", null, null).build();
+      VALID_OM_LC_AND_OPERATOR =
+          getOmLCAndOperatorBuilder("prefix", Collections.singletonMap("tag1", 
"value1")).build();
+    } catch (OMException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static void assertOMException(Executable action, 
OMException.ResultCodes expectedResultCode,
+      String expectedMessageContent) {
+    OMException e = assertThrows(OMException.class, action);
+    assertEquals(expectedResultCode, e.getResult());
+    assertTrue(e.getMessage().contains(expectedMessageContent),
+        "Expected: " + expectedMessageContent + "\n Actual: " + 
e.getMessage());
+  }
+
+  public static String getFutureDateString(long daysInFuture, int 
hoursInFuture, int minuteInFuture) {
+    return ZonedDateTime.now(ZoneOffset.UTC)
+        .plusDays(daysInFuture)
+        .plusHours(hoursInFuture)
+        .plusMinutes(minuteInFuture)
+        .withSecond(0)
+        .withNano(0)
+        .format(DateTimeFormatter.ISO_DATE_TIME);
+  }
+
+  public static String getFutureDateString(long daysInFuture) {
+    return ZonedDateTime.now(ZoneOffset.UTC)
+        .plusDays(daysInFuture)
+        .withHour(0)
+        .withMinute(0)
+        .withSecond(0)
+        .withNano(0)
+        .format(DateTimeFormatter.ISO_DATE_TIME);
+  }
+
+  public static OmLifecycleConfiguration.Builder getOmLifecycleConfiguration(
+      String volume, String bucket, List<OmLCRule> rules) {
+    return new OmLifecycleConfiguration.Builder()
+        .setVolume(volume)
+        .setBucket(bucket)
+        .setRules(rules);
+  }
+
+  public static OmLCRule.Builder getOmLCRuleBuilder(String id, String prefix, 
boolean enabled,
+                                                    int expirationDays, 
OmLCFilter filter) throws OMException {
+    OmLCRule.Builder rBuilder = new OmLCRule.Builder()
+        .setEnabled(enabled)
+        .setId(id)
+        .setPrefix(prefix)
+        .setFilter(filter);
+
+    if (expirationDays > 0) {
+      rBuilder.setAction(new OmLCExpiration.Builder()
+          .setDays(expirationDays).build());
+    }
+
+    return rBuilder;
+  }
+
+  public static OmLCFilter.Builder getOmLCFilterBuilder(String filterPrefix, 
Pair<String, String> filterTag,
+                                                OmLifecycleRuleAndOperator 
andOperator) {
+    OmLCFilter.Builder lcfBuilder = new OmLCFilter.Builder()
+        .setPrefix(filterPrefix)
+        .setAndOperator(andOperator);
+    if (filterTag != null) {
+      lcfBuilder.setTag(filterTag.getKey(), filterTag.getValue());
+    }
+    return lcfBuilder;
+  }
+
+  public static OmLifecycleRuleAndOperator.Builder getOmLCAndOperatorBuilder(
+      String prefix, Map<String, String> tags) {
+    return new OmLifecycleRuleAndOperator.Builder()
+        .setPrefix(prefix)
+        .setTags(tags);
+  }
+
+  private OMLCUtils() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java
new file mode 100644
index 0000000000..1a51056d01
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCExpiration.java
@@ -0,0 +1,167 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.assertOMException;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.getFutureDateString;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test OmLCExpiration.
+ */
+class TestOmLCExpiration {
+
+  @Test
+  public void testCreateValidOmLCExpiration() {
+    OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder()
+        .setDays(30);
+    assertDoesNotThrow(exp1::build);
+
+    OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:00:00Z");
+    assertDoesNotThrow(exp2::build);
+
+    OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder()
+        .setDays(1);
+    assertDoesNotThrow(exp3::build);
+
+    OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder()
+        .setDate("2099-12-31T00:00:00Z");
+    assertDoesNotThrow(exp4::build);
+
+    OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder()
+        .setDate("2099-02-15T00:00:00.000Z");
+    assertDoesNotThrow(exp5::build);
+
+    OmLCExpiration.Builder exp6 = new OmLCExpiration.Builder()
+        .setDate("2042-04-02T00:00:00Z");
+    assertDoesNotThrow(exp6::build);
+
+    OmLCExpiration.Builder exp7 = new OmLCExpiration.Builder()
+        .setDate("2042-04-02T00:00:00+00:00");
+    assertDoesNotThrow(exp7::build);
+
+    OmLCExpiration.Builder exp8 = new OmLCExpiration.Builder()
+        .setDate("2099-12-31T00:00:00+00:00");
+    assertDoesNotThrow(exp8::build);
+
+    OmLCExpiration.Builder exp9 = new OmLCExpiration.Builder()
+        .setDate("2099-12-31T23:00:00-01:00");
+    assertDoesNotThrow(exp9::build);
+
+    OmLCExpiration.Builder exp10 = new OmLCExpiration.Builder()
+        .setDate("2100-01-01T01:00:00+01:00");
+    assertDoesNotThrow(exp10::build);
+
+    OmLCExpiration.Builder exp11 = new OmLCExpiration.Builder()
+        .setDate("2099-12-31T12:00:00-12:00");
+    assertDoesNotThrow(exp11::build);
+
+    OmLCExpiration.Builder exp12 = new OmLCExpiration.Builder()
+        .setDate("2100-01-01T12:00:00+12:00");
+    assertDoesNotThrow(exp12::build);
+  }
+
+  @Test
+  public void testCreateInValidOmLCExpiration() {
+    OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder()
+        .setDays(30)
+        .setDate(getFutureDateString(100));
+    assertOMException(exp1::build, INVALID_REQUEST,
+        "Either 'days' or 'date' should be specified, but not both or 
neither.");
+
+    OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder()
+        .setDays(-1);
+    assertOMException(exp2::build, INVALID_REQUEST,
+        "'Days' for Expiration action must be a positive integer");
+
+    OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder()
+        .setDate(null);
+    assertOMException(exp3::build, INVALID_REQUEST,
+        "Either 'days' or 'date' should be specified, but not both or 
neither.");
+
+    OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder()
+        .setDate("");
+    assertOMException(exp4::build, INVALID_REQUEST,
+        "Either 'days' or 'date' should be specified, but not both or 
neither.");
+
+    OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder();
+    assertOMException(exp5::build, INVALID_REQUEST,
+        "Either 'days' or 'date' should be specified, but not both or 
neither.");
+
+    OmLCExpiration.Builder exp6 = new OmLCExpiration.Builder()
+        .setDate("10-10-2099");
+    assertOMException(exp6::build, INVALID_REQUEST,
+        "'Date' must be in ISO 8601 format");
+
+    OmLCExpiration.Builder exp7 = new OmLCExpiration.Builder()
+        .setDate("2099-12-31T00:00:00");
+    assertOMException(exp7::build, INVALID_REQUEST,
+        "'Date' must be in ISO 8601 format");
+
+    // Testing for date in the past
+    OmLCExpiration.Builder exp8 = new OmLCExpiration.Builder()
+        .setDate(getFutureDateString(-1));
+    assertOMException(exp8::build, INVALID_REQUEST,
+        "'Date' must be in the future");
+
+    OmLCExpiration.Builder exp9 = new OmLCExpiration.Builder()
+        .setDays(0);
+    assertOMException(exp9::build, INVALID_REQUEST,
+        "'Days' for Expiration action must be a positive integer");
+
+    // 1 minute ago
+    OmLCExpiration.Builder exp10 = new OmLCExpiration.Builder()
+        .setDate(getFutureDateString(0, 0, -1));
+    assertOMException(exp10::build, INVALID_REQUEST,
+        "'Date' must be in the future");
+  }
+
+  @Test
+  public void testDateMustBeAtMidnightUTC() {
+    // Acceptable date - midnight UTC
+    OmLCExpiration.Builder validExp = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:00:00Z");
+    assertDoesNotThrow(validExp::build);
+
+    // Non-midnight UTC dates should be rejected
+    OmLCExpiration.Builder exp1 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T10:00:00Z");
+    assertOMException(exp1::build, INVALID_REQUEST, "'Date' must represent 
midnight UTC");
+
+    OmLCExpiration.Builder exp2 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:30:00Z");
+    assertOMException(exp2::build, INVALID_REQUEST, "'Date' must represent 
midnight UTC");
+
+    OmLCExpiration.Builder exp3 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:00:30Z");
+    assertOMException(exp3::build, INVALID_REQUEST, "'Date' must represent 
midnight UTC");
+
+    OmLCExpiration.Builder exp4 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:00:00.123Z");
+    assertOMException(exp4::build, INVALID_REQUEST, "'Date' must represent 
midnight UTC");
+
+    // Non-UTC timezone should be rejected
+    OmLCExpiration.Builder exp5 = new OmLCExpiration.Builder()
+        .setDate("2099-10-10T00:00:00+01:00");
+    assertOMException(exp5::build, INVALID_REQUEST, "'Date' must represent 
midnight UTC");
+  }
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java
new file mode 100644
index 0000000000..67974069e5
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCFilter.java
@@ -0,0 +1,84 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.VALID_OM_LC_AND_OPERATOR;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.VALID_OM_LC_FILTER;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.assertOMException;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCFilterBuilder;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCRuleBuilder;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test OmLCExpiration.
+ */
+class TestOmLCFilter {
+
+  @Test
+  public void testInValidOmLCRulePrefixFilterCoExist() throws OMException {
+    OmLCRule.Builder rule1 = getOmLCRuleBuilder("id", "prefix", true, 1, 
VALID_OM_LC_FILTER);
+    assertOMException(rule1::build, INVALID_REQUEST, "Filter and Prefix cannot 
be used together");
+
+    OmLCRule.Builder rule2 = getOmLCRuleBuilder("id", "", true, 1, 
VALID_OM_LC_FILTER);
+    assertOMException(rule2::build, INVALID_REQUEST, "Filter and Prefix cannot 
be used together");
+  }
+
+  @Test
+  public void testValidFilter() throws OMException {
+    OmLCFilter lcFilter1 = getOmLCFilterBuilder("prefix", null, null).build();
+    assertDoesNotThrow(lcFilter1::valid);
+
+    OmLCFilter lcFilter2 = getOmLCFilterBuilder(null, Pair.of("key", "value"), 
null).build();
+    assertDoesNotThrow(lcFilter2::valid);
+
+    OmLCFilter lcFilter3 = getOmLCFilterBuilder(null, null, 
VALID_OM_LC_AND_OPERATOR).build();
+    assertDoesNotThrow(lcFilter3::valid);
+
+    OmLCFilter lcFilter4 = getOmLCFilterBuilder(null, null, null).build();
+    assertDoesNotThrow(lcFilter4::valid);
+
+    OmLCFilter lcFilter5 = getOmLCFilterBuilder("", null, null).build();
+    assertDoesNotThrow(lcFilter5::valid);
+  }
+
+  @Test
+  public void testInValidFilter() {
+    OmLCFilter.Builder lcFilter1 = getOmLCFilterBuilder("prefix", 
Pair.of("key", "value"), VALID_OM_LC_AND_OPERATOR);
+    assertOMException(lcFilter1::build, INVALID_REQUEST,
+        "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified");
+
+    OmLCFilter.Builder lcFilter2 = getOmLCFilterBuilder("prefix", 
Pair.of("key", "value"), null);
+    assertOMException(lcFilter2::build, INVALID_REQUEST,
+        "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified");
+
+    OmLCFilter.Builder lcFilter3 = getOmLCFilterBuilder("prefix", null, 
VALID_OM_LC_AND_OPERATOR);
+    assertOMException(lcFilter3::build, INVALID_REQUEST,
+        "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified");
+
+    OmLCFilter.Builder lcFilter4 = getOmLCFilterBuilder(null, Pair.of("key", 
"value"), VALID_OM_LC_AND_OPERATOR);
+    assertOMException(lcFilter4::build, INVALID_REQUEST,
+        "Only one of 'Prefix', 'Tag', or 'AndOperator' should be specified");
+
+  }
+
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java
new file mode 100644
index 0000000000..39e3cab736
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLCRule.java
@@ -0,0 +1,171 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.assertOMException;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCAndOperatorBuilder;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCFilterBuilder;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test OmLCRule.
+ */
+class TestOmLCRule {
+
+  @Test
+  public void testCreateValidOmLCRule() throws OMException {
+    OmLCExpiration exp = new OmLCExpiration.Builder()
+        .setDays(30)
+        .build();
+
+    OmLCRule.Builder r1 = new OmLCRule.Builder()
+        .setId("remove Spark logs after 30 days")
+        .setEnabled(true)
+        .setPrefix("/spark/logs")
+        .setAction(exp);
+    assertDoesNotThrow(r1::build);
+
+    OmLCRule.Builder r2 = new OmLCRule.Builder()
+        .setEnabled(true)
+        .setPrefix("")
+        .setAction(exp);
+    assertDoesNotThrow(r2::build);
+
+    // Empty id should generate a 48 (default) bit one.
+    OmLCRule.Builder r3 = new OmLCRule.Builder()
+        .setEnabled(true)
+        .setAction(exp);
+
+    OmLCRule omLCRule = assertDoesNotThrow(r3::build);
+    assertEquals(OmLCRule.LC_ID_LENGTH, omLCRule.getId().length(),
+        "Expected a " + OmLCRule.LC_ID_LENGTH + " length generated ID");
+  }
+
+  @Test
+  public void testCreateInValidOmLCRule() throws OMException {
+    OmLCExpiration exp = new OmLCExpiration.Builder()
+        .setDays(30)
+        .build();
+
+    char[] id = new char[OmLCRule.LC_ID_MAX_LENGTH + 1];
+    Arrays.fill(id, 'a');
+
+    OmLCRule.Builder r1 = new OmLCRule.Builder()
+        .setId(new String(id))
+        .setAction(exp);
+    assertOMException(r1::build, INVALID_REQUEST, "ID length should not exceed 
allowed limit of 255");
+
+    OmLCRule.Builder r2 = new OmLCRule.Builder()
+        .setId("remove Spark logs after 30 days")
+        .setEnabled(true)
+        .setPrefix("/spark/logs")
+        .setAction(null);
+    assertOMException(r2::build, INVALID_REQUEST,
+        "At least one action needs to be specified in a rule");
+  }
+
+  @Test
+  public void testMultipleActionsInRule() throws OMException {
+    OmLCExpiration expiration1 = new OmLCExpiration.Builder()
+        .setDays(30)
+        .build();
+
+    OmLCExpiration expiration2 = new OmLCExpiration.Builder()
+        .setDays(60)
+        .build();
+
+    List<OmLCAction> actions = new ArrayList<>();
+    actions.add(expiration1);
+    actions.add(expiration2);
+
+    OmLCRule.Builder builder = new OmLCRule.Builder();
+    builder.setId("test-rule");
+
+    OmLCRule.Builder rule = builder.setActions(actions);
+
+    assertOMException(rule::build, INVALID_REQUEST, "A rule can have at most 
one Expiration action");
+  }
+
+  @Test
+  public void testRuleWithAndOperatorFilter() throws OMException {
+    Map<String, String> tags = ImmutableMap.of("app", "hadoop", "env", "test");
+    OmLifecycleRuleAndOperator andOperator = 
getOmLCAndOperatorBuilder("/logs/", tags).build();
+    OmLCFilter filter = getOmLCFilterBuilder(null, null, andOperator).build();
+
+    OmLCRule.Builder builder = new OmLCRule.Builder()
+        .setId("and-operator-rule")
+        .setEnabled(true)
+        .setFilter(filter)
+        .setAction(new OmLCExpiration.Builder().setDays(30).build());
+
+    OmLCRule rule = assertDoesNotThrow(builder::build);
+    assertTrue(rule.isPrefixEnable());
+    assertTrue(rule.isTagEnable());
+  }
+
+  @Test
+  public void testRuleWithTagFilter() throws OMException {
+    OmLCFilter filter = getOmLCFilterBuilder(null, Pair.of("app", "hadoop"), 
null).build();
+
+    OmLCRule.Builder builder = new OmLCRule.Builder()
+        .setId("tag-filter-rule")
+        .setEnabled(true)
+        .setFilter(filter)
+        .setAction(new OmLCExpiration.Builder().setDays(30).build());
+
+    OmLCRule rule = assertDoesNotThrow(builder::build);
+    assertFalse(rule.isPrefixEnable());
+    assertTrue(rule.isTagEnable());
+  }
+
+  @Test
+  public void testDuplicateRuleIDs() throws OMException {
+    List<OmLCRule> rules = new ArrayList<>();
+
+    rules.add(new OmLCRule.Builder()
+        .setId("duplicate-id")
+        .setAction(new OmLCExpiration.Builder().setDays(30).build())
+        .build());
+
+    rules.add(new OmLCRule.Builder()
+        .setId("duplicate-id") // Same ID
+        .setAction(new OmLCExpiration.Builder().setDays(60).build())
+        .build());
+
+    OmLifecycleConfiguration.Builder config = new 
OmLifecycleConfiguration.Builder()
+        .setVolume("volume")
+        .setBucket("bucket")
+        .setRules(rules);
+
+    assertOMException(config::build, INVALID_REQUEST, "Duplicate rule IDs 
found");
+  }
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java
new file mode 100644
index 0000000000..8be22c6e82
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifeCycleConfiguration.java
@@ -0,0 +1,191 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.assertOMException;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.getFutureDateString;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCAndOperatorBuilder;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCFilterBuilder;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLifecycleConfiguration;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test lifecycle configuration related entities.
+ */
+public class TestOmLifeCycleConfiguration {
+
+  @Test
+  public void testCreateValidLCConfiguration() throws OMException {
+    OmLifecycleConfiguration lcc = new OmLifecycleConfiguration.Builder()
+        .setVolume("s3v")
+        .setBucket("spark")
+        .setRules(Collections.singletonList(new OmLCRule.Builder()
+            .setId("spark logs")
+            .setAction(new OmLCExpiration.Builder()
+                .setDays(30)
+                .build())
+            .build()))
+        .build();
+
+    assertDoesNotThrow(lcc::valid);
+  }
+
+  @Test
+  public void testCreateInValidLCConfiguration() throws OMException {
+    OmLCRule rule = new OmLCRule.Builder()
+        .setId("spark logs")
+        .setAction(new OmLCExpiration.Builder().setDays(30).build())
+        .build();
+
+    List<OmLCRule> rules = Collections.singletonList(rule);
+
+    OmLifecycleConfiguration.Builder lcc0 = getOmLifecycleConfiguration(null, 
"bucket", rules);
+    assertOMException(lcc0::build, INVALID_REQUEST, "Volume cannot be blank");
+
+    OmLifecycleConfiguration.Builder lcc1 = 
getOmLifecycleConfiguration("volume", null, rules);
+    assertOMException(lcc1::build, INVALID_REQUEST, "Bucket cannot be blank");
+
+    OmLifecycleConfiguration.Builder lcc3 = getOmLifecycleConfiguration(
+        "volume", "bucket", Collections.emptyList());
+    assertOMException(lcc3::build, INVALID_REQUEST,
+        "At least one rules needs to be specified in a lifecycle 
configuration");
+
+    List<OmLCRule> rules4 = new ArrayList<>(
+        OmLifecycleConfiguration.LC_MAX_RULES + 1);
+    for (int i = 0; i < OmLifecycleConfiguration.LC_MAX_RULES + 1; i++) {
+      OmLCRule r = new OmLCRule.Builder()
+          .setId(Integer.toString(i))
+          .setAction(new OmLCExpiration.Builder().setDays(30).build())
+          .build();
+      rules4.add(r);
+    }
+    OmLifecycleConfiguration.Builder lcc4 = 
getOmLifecycleConfiguration("volume", "bucket", rules4);
+    assertOMException(lcc4::build, INVALID_REQUEST,
+        "The number of lifecycle rules must not exceed the allowed limit of");
+  }
+
+  @Test
+  public void testToBuilder() throws OMException {
+    String volume = "test-volume";
+    String bucket = "test-bucket";
+    long creationTime = System.currentTimeMillis();
+    long objectID = 123456L;
+    long updateID = 78910L;
+
+    OmLCRule rule1 = new OmLCRule.Builder()
+        .setId("test-rule1")
+        .setAction(new OmLCExpiration.Builder().setDays(30).build())
+        .build();
+
+    OmLCRule rule2 = new OmLCRule.Builder()
+        .setId("test-rule2")
+        .setAction(new OmLCExpiration.Builder().setDays(60).build())
+        .build();
+
+    OmLifecycleConfiguration originalConfig = new 
OmLifecycleConfiguration.Builder()
+        .setVolume(volume)
+        .setBucket(bucket)
+        .setCreationTime(creationTime)
+        .addRule(rule1)
+        .addRule(rule2)
+        .setObjectID(objectID)
+        .setUpdateID(updateID)
+        .build();
+
+    OmLifecycleConfiguration.Builder builder = originalConfig.toBuilder();
+    OmLifecycleConfiguration rebuiltConfig = builder.build();
+
+    assertEquals(volume, rebuiltConfig.getVolume());
+    assertEquals(bucket, rebuiltConfig.getBucket());
+    assertEquals(creationTime, rebuiltConfig.getCreationTime());
+    assertEquals(2, rebuiltConfig.getRules().size());
+    assertEquals(rule1.getId(), rebuiltConfig.getRules().get(0).getId());
+    assertEquals(rule2.getId(), rebuiltConfig.getRules().get(1).getId());
+    assertEquals(objectID, rebuiltConfig.getObjectID());
+    assertEquals(updateID, rebuiltConfig.getUpdateID());
+  }
+
+  @Test
+  public void testComplexLifecycleConfiguration() throws OMException {
+    List<OmLCRule> rules = new ArrayList<>();
+
+    // Rule 1: Simple expiration by days with prefix
+    rules.add(new OmLCRule.Builder()
+        .setId("rule1")
+        .setEnabled(true)
+        .setPrefix("/logs/")
+        .setAction(new OmLCExpiration.Builder().setDays(30).build())
+        .build());
+
+    // Rule 2: Expiration by date with tag filter
+    rules.add(new OmLCRule.Builder()
+        .setId("rule2")
+        .setEnabled(true)
+        .setFilter(getOmLCFilterBuilder(null, Pair.of("temporary", "true"), 
null).build())
+        .setAction(new OmLCExpiration.Builder()
+            .setDate(getFutureDateString(100))
+            .build())
+        .build());
+
+    // Rule 3: Expiration with complex AND filter
+    rules.add(new OmLCRule.Builder()
+        .setId("rule3")
+        .setEnabled(true)
+        .setFilter(getOmLCFilterBuilder(null, null,
+            getOmLCAndOperatorBuilder("/backups/",
+                ImmutableMap.of("tier", "archive", "retention", "short"))
+                .build())
+            .build())
+        .setAction(new OmLCExpiration.Builder().setDays(365).build())
+        .build());
+
+    OmLifecycleConfiguration config = new OmLifecycleConfiguration.Builder()
+        .setVolume("test-volume")
+        .setBucket("test-bucket")
+        .setRules(rules)
+        .build();
+
+    assertDoesNotThrow(config::valid);
+    assertEquals(3, config.getRules().size());
+  }
+
+  @Test
+  public void testDisabledRule() throws OMException {
+    OmLCRule rule = new OmLCRule.Builder()
+        .setId("disabled-rule")
+        .setEnabled(false) // Explicitly disabled
+        .setPrefix("/temp/")
+        .setAction(new OmLCExpiration.Builder().setDays(7).build())
+        .build();
+
+    assertFalse(rule.isEnabled());
+    assertDoesNotThrow(rule::valid);
+  }
+
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java
new file mode 100644
index 0000000000..ffc47dffdf
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmLifecycleRuleAndOperator.java
@@ -0,0 +1,70 @@
+/*
+ * 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.hadoop.ozone.om.helpers;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static org.apache.hadoop.ozone.om.helpers.OMLCUtils.assertOMException;
+import static 
org.apache.hadoop.ozone.om.helpers.OMLCUtils.getOmLCAndOperatorBuilder;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test OmLifecycleRuleAndOperator.
+ */
+class TestOmLifecycleRuleAndOperator {
+
+  @Test
+  public void testValidAndOperator() throws OMException {
+    OmLifecycleRuleAndOperator andOperator1 =
+        getOmLCAndOperatorBuilder("prefix", Collections.singletonMap("tag1", 
"value1")).build();
+    assertDoesNotThrow(andOperator1::valid);
+
+    OmLifecycleRuleAndOperator andOperator2 =
+        getOmLCAndOperatorBuilder("", Collections.singletonMap("tag1", 
"value1")).build();
+    assertDoesNotThrow(andOperator2::valid);
+
+    OmLifecycleRuleAndOperator andOperator3 = getOmLCAndOperatorBuilder(
+        "prefix", ImmutableMap.of("tag1", "value1", "tag2", "value2")).build();
+    assertDoesNotThrow(andOperator3::valid);
+
+    OmLifecycleRuleAndOperator andOperator4 = getOmLCAndOperatorBuilder(
+        null, ImmutableMap.of("tag1", "value1", "tag2", "value2")).build();
+    assertDoesNotThrow(andOperator4::valid);
+
+
+  }
+
+  @Test
+  public void testInValidAndOperator() {
+    OmLifecycleRuleAndOperator.Builder andOperator1 = 
getOmLCAndOperatorBuilder("prefix", null);
+    assertOMException(andOperator1::build, INVALID_REQUEST, "'Prefix' alone is 
not allowed");
+
+    OmLifecycleRuleAndOperator.Builder andOperator2 =
+        getOmLCAndOperatorBuilder(null, Collections.singletonMap("tag1", 
"value1"));
+    assertOMException(andOperator2::build, INVALID_REQUEST,
+        "If 'Tags' are specified without 'Prefix', there should be more than 
one tag");
+
+    OmLifecycleRuleAndOperator.Builder andOperator3 = 
getOmLCAndOperatorBuilder(null, null);
+    assertOMException(andOperator3::build, INVALID_REQUEST, "Either 'Tags' or 
'Prefix' must be specified.");
+  }
+
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to