001package com.streamconverter.command.rule.impl.composite;
002
003import com.streamconverter.command.rule.IRule;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.List;
007
008/**
009 * Chains multiple transformation rules to be applied in sequence.
010 *
011 * <p>This rule allows combining multiple {@link IRule} instances to create complex transformations
012 * by applying them one after another. The output of each rule becomes the input for the next rule
013 * in the chain.
014 *
015 * <p>Examples:
016 *
017 * <pre>{@code
018 * // Trim whitespace then convert to snake_case
019 * IRule chainedRule = ChainRule.builder()
020 *     .addRule(new TrimRule())
021 *     .addRule(CamelToSnakeCaseRule.create())
022 *     .build();
023 *
024 * // Multiple transformations
025 * IRule complexRule = ChainRule.of(
026 *     new TrimRule(),
027 *     new LowerCaseRule(),
028 *     CamelToSnakeCaseRule.create()
029 * );
030 * }</pre>
031 *
032 * <p>Thread Safety: This class is thread-safe as long as the individual rules in the chain are
033 * thread-safe.
034 *
035 * @since 1.0
036 */
037public class ChainRule implements IRule {
038
039  /** List of rules to apply in sequence */
040  private final List<IRule> rules;
041
042  /** Private constructor for builder pattern */
043  private ChainRule(List<IRule> rules) {
044    this.rules = Collections.unmodifiableList(new ArrayList<>(rules));
045  }
046
047  @Override
048  public String apply(String input) {
049    String result = input;
050
051    // Apply each rule in sequence
052    for (IRule rule : rules) {
053      result = rule.apply(result);
054    }
055
056    return result;
057  }
058
059  /**
060   * Gets the number of rules in this chain.
061   *
062   * @return number of rules
063   */
064  public int size() {
065    return rules.size();
066  }
067
068  /**
069   * Gets an unmodifiable view of the rules in this chain.
070   *
071   * @return unmodifiable list of rules
072   */
073  public List<IRule> getRules() {
074    return rules;
075  }
076
077  /**
078   * Creates a builder for configuring the ChainRule.
079   *
080   * @return new builder instance
081   */
082  public static Builder builder() {
083    return new Builder();
084  }
085
086  /**
087   * Creates a ChainRule with the specified rules.
088   *
089   * @param rules rules to chain together
090   * @return new ChainRule instance
091   * @throws IllegalArgumentException if rules array is null or empty
092   */
093  public static ChainRule of(IRule... rules) {
094    if (rules == null || rules.length == 0) {
095      throw new IllegalArgumentException("Chain must contain at least one rule");
096    }
097    return new Builder().addRules(rules).build();
098  }
099
100  /**
101   * Creates a ChainRule with the specified rules.
102   *
103   * @param rules list of rules to chain together
104   * @return new ChainRule instance
105   * @throws IllegalArgumentException if rules list is null or empty
106   */
107  public static ChainRule of(List<IRule> rules) {
108    return new Builder().addRules(rules).build();
109  }
110
111  /** Builder class for ChainRule configuration. */
112  public static class Builder {
113    private final List<IRule> rules = new ArrayList<>();
114
115    /**
116     * Adds a rule to the end of the chain.
117     *
118     * @param rule rule to add (null rules are ignored)
119     * @return this builder
120     */
121    public Builder addRule(IRule rule) {
122      if (rule != null) {
123        this.rules.add(rule);
124      }
125      return this;
126    }
127
128    /**
129     * Adds multiple rules to the end of the chain.
130     *
131     * @param rules rules to add (null rules are ignored)
132     * @return this builder
133     */
134    public Builder addRules(IRule... rules) {
135      if (rules != null) {
136        for (IRule rule : rules) {
137          addRule(rule);
138        }
139      }
140      return this;
141    }
142
143    /**
144     * Adds multiple rules to the end of the chain.
145     *
146     * @param rules list of rules to add (null rules are ignored)
147     * @return this builder
148     */
149    public Builder addRules(List<IRule> rules) {
150      if (rules != null) {
151        for (IRule rule : rules) {
152          addRule(rule);
153        }
154      }
155      return this;
156    }
157
158    /**
159     * Inserts a rule at the specified position in the chain.
160     *
161     * @param index position to insert at
162     * @param rule rule to insert
163     * @return this builder
164     * @throws IndexOutOfBoundsException if index is out of range
165     */
166    public Builder insertRule(int index, IRule rule) {
167      if (rule != null) {
168        this.rules.add(index, rule);
169      }
170      return this;
171    }
172
173    /**
174     * Removes all rules from the chain.
175     *
176     * @return this builder
177     */
178    public Builder clear() {
179      this.rules.clear();
180      return this;
181    }
182
183    /**
184     * Gets the current number of rules in the builder.
185     *
186     * @return number of rules
187     */
188    public int size() {
189      return rules.size();
190    }
191
192    /**
193     * Builds the ChainRule with current configuration.
194     *
195     * @return configured rule instance
196     * @throws IllegalArgumentException if no rules have been added
197     */
198    public ChainRule build() {
199      if (rules.isEmpty()) {
200        throw new IllegalArgumentException("Chain must contain at least one rule");
201      }
202      return new ChainRule(rules);
203    }
204  }
205
206  @Override
207  public String toString() {
208    return String.format("ChainRule{rules=%d, chain=%s}", rules.size(), rules);
209  }
210
211  @Override
212  public boolean equals(Object obj) {
213    if (this == obj) return true;
214    if (obj == null || getClass() != obj.getClass()) return false;
215    ChainRule chainRule = (ChainRule) obj;
216    return rules.equals(chainRule.rules);
217  }
218
219  @Override
220  public int hashCode() {
221    return rules.hashCode();
222  }
223}