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]