001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package org.basepom.mojo.propertyhelper.fields;
016
017import static com.google.common.base.Preconditions.checkState;
018
019import org.basepom.mojo.propertyhelper.Field;
020import org.basepom.mojo.propertyhelper.FieldContext;
021import org.basepom.mojo.propertyhelper.ValueProvider;
022import org.basepom.mojo.propertyhelper.definitions.NumberDefinition;
023
024import java.util.List;
025import java.util.Optional;
026import java.util.StringJoiner;
027
028import com.google.common.annotations.VisibleForTesting;
029import com.google.common.base.Joiner;
030import com.google.common.collect.ImmutableList;
031
032public final class NumberField extends Field<String, NumberDefinition> {
033
034    private final ValueProvider valueProvider;
035
036    // internal state
037    private List<NumberElement> numberElements;
038    private List<NumberElement> numberIndex;
039
040    @VisibleForTesting
041    public static NumberField forTesting(NumberDefinition numberDefinition, ValueProvider valueProvider) {
042        return new NumberField(numberDefinition, valueProvider, FieldContext.forTesting());
043    }
044
045    public NumberField(final NumberDefinition fieldDefinition, final ValueProvider valueProvider,
046        FieldContext fieldContext) {
047        super(fieldDefinition, fieldContext);
048
049        this.valueProvider = valueProvider;
050    }
051
052    @Override
053    public String getFieldName() {
054        // This is not the property name (because many definitions can map onto one prop)
055        // but the actual id.
056        return fieldDefinition.getId();
057    }
058
059    @Override
060    @SuppressWarnings("PMD.LambdaCanBeMethodReference") // https://github.com/pmd/pmd/issues/5043
061    public String getValue() {
062        parse();
063
064        return formatResult(value().map(v -> Long.toString(v))
065            .orElse(valueProvider.getValue().orElse("")));
066    }
067
068    private void parse() {
069        final Optional<String> value = valueProvider.getValue();
070
071        ImmutableList.Builder<NumberElement> numberELementBuilder = ImmutableList.builder();
072        ImmutableList.Builder<NumberElement> numberIndexBuilder = ImmutableList.builder();
073
074        if (value.isPresent()) {
075            String numberValue = value.get();
076            if (!numberValue.isBlank()) {
077                StringBuilder sb = new StringBuilder();
078                int charIndex = 0;
079                boolean number = Character.isDigit(numberValue.charAt(charIndex));
080
081                while (charIndex < numberValue.length()) {
082                    char currentChar = numberValue.charAt(charIndex);
083                    if (number != Character.isDigit(currentChar)) {
084                        var numberElement = new NumberElement(number, sb.toString());
085
086                        numberELementBuilder.add(numberElement);
087                        if (number) {
088                            numberIndexBuilder.add(numberElement);
089                        }
090
091                        number = !number;
092                        sb.setLength(0);
093                    }
094                    sb.append(currentChar);
095                    charIndex++;
096                }
097                if (sb.length() > 0) {
098                    var numberElement = new NumberElement(number, sb.toString());
099
100                    numberELementBuilder.add(numberElement);
101                    if (number) {
102                        numberIndexBuilder.add(numberElement);
103                    }
104                }
105            }
106        }
107
108        this.numberElements = numberELementBuilder.build();
109        this.numberIndex = numberIndexBuilder.build();
110    }
111
112    private String print() {
113        return Joiner.on("").join(numberElements.stream().map(NumberElement::getFieldValue).iterator());
114    }
115
116    private Optional<Long> value() {
117        return fieldDefinition.getFieldNumber()
118            .map(numberIndex::get)
119            .flatMap(NumberElement::getLongValue);
120    }
121
122    private void set(long value) {
123        Optional<NumberElement> numberElement = fieldDefinition.getFieldNumber()
124            .map(numberIndex::get);
125
126        if (numberElement.isPresent()) {
127            numberElement.get().setLongValue(value);
128            valueProvider.setValue(print());
129        } else {
130            valueProvider.setValue(Long.toString(value));
131        }
132    }
133
134    public void increment() {
135        parse();
136
137        fieldDefinition.getFieldNumber().ifPresent(fieldNumber -> checkState(numberIndex.size() > fieldNumber,
138            "Only %s fields in %s, field %s requested.", numberElements.size(), print(), fieldNumber));
139
140        value().ifPresent(value -> {
141            set(value + fieldDefinition.getIncrement());
142        });
143    }
144
145    public Optional<Long> getNumberValue() {
146        parse();
147
148        fieldDefinition.getFieldNumber().ifPresent(fieldNumber -> checkState(numberIndex.size() > fieldNumber,
149            "Only %s fields in %s, field %s requested.", numberElements.size(), print(), fieldNumber));
150
151        return value();
152    }
153
154    @Override
155    public String toString() {
156        return new StringJoiner(", ", NumberField.class.getSimpleName() + "[", "]")
157            .add("valueProvider=" + valueProvider)
158            .add("numberElements=" + numberElements)
159            .add("numberIndex=" + numberIndex)
160            .add("fieldDefinition=" + fieldDefinition)
161            .toString();
162    }
163
164    private static final class NumberElement {
165
166        private final boolean number;
167        private String value;
168
169        private NumberElement(boolean number, String value) {
170            this.number = number;
171            this.value = value;
172        }
173
174        String getFieldValue() {
175            return value;
176        }
177
178        void setLongValue(long value) {
179            this.value = Long.toString(value);
180        }
181
182        Optional<Long> getLongValue() {
183            return number ? Optional.of(Long.parseLong(value)) : Optional.empty();
184        }
185
186        @Override
187        public String toString() {
188            return new StringJoiner(", ", NumberElement.class.getSimpleName() + "[", "]")
189                .add("number=" + number)
190                .add("value='" + value + "'")
191                .toString();
192        }
193    }
194}