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    public String getValue() {
061        parse();
062
063        return formatResult(value().map(v -> Long.toString(v))
064            .orElse(valueProvider.getValue().orElse("")));
065    }
066
067    private void parse() {
068        final Optional<String> value = valueProvider.getValue();
069
070        ImmutableList.Builder<NumberElement> numberELementBuilder = ImmutableList.builder();
071        ImmutableList.Builder<NumberElement> numberIndexBuilder = ImmutableList.builder();
072
073        if (value.isPresent()) {
074            String numberValue = value.get();
075            if (!numberValue.isBlank()) {
076                StringBuilder sb = new StringBuilder();
077                int charIndex = 0;
078                boolean number = Character.isDigit(numberValue.charAt(charIndex));
079
080                while (charIndex < numberValue.length()) {
081                    char currentChar = numberValue.charAt(charIndex);
082                    if (number != Character.isDigit(currentChar)) {
083                        var numberElement = new NumberElement(number, sb.toString());
084
085                        numberELementBuilder.add(numberElement);
086                        if (number) {
087                            numberIndexBuilder.add(numberElement);
088                        }
089
090                        number = !number;
091                        sb.setLength(0);
092                    }
093                    sb.append(currentChar);
094                    charIndex++;
095                }
096                if (sb.length() > 0) {
097                    var numberElement = new NumberElement(number, sb.toString());
098
099                    numberELementBuilder.add(numberElement);
100                    if (number) {
101                        numberIndexBuilder.add(numberElement);
102                    }
103                }
104            }
105        }
106
107        this.numberElements = numberELementBuilder.build();
108        this.numberIndex = numberIndexBuilder.build();
109    }
110
111    private String print() {
112        return Joiner.on("").join(numberElements.stream().map(NumberElement::getFieldValue).iterator());
113    }
114
115    private Optional<Long> value() {
116        return fieldDefinition.getFieldNumber()
117            .map(fieldNumber -> numberIndex.get(fieldNumber))
118            .flatMap(NumberElement::getLongValue);
119    }
120
121    private void set(long value) {
122        Optional<NumberElement> numberElement = fieldDefinition.getFieldNumber()
123            .map(fieldNumber -> numberIndex.get(fieldNumber));
124
125        if (numberElement.isPresent()) {
126            numberElement.get().setLongValue(value);
127            valueProvider.setValue(print());
128        } else {
129            valueProvider.setValue(Long.toString(value));
130        }
131    }
132
133    public void increment() {
134        parse();
135
136        fieldDefinition.getFieldNumber().ifPresent(fieldNumber -> checkState(numberIndex.size() > fieldNumber,
137            "Only %s fields in %s, field %s requested.", numberElements.size(), print(), fieldNumber));
138
139        value().ifPresent(value -> {
140            set(value + fieldDefinition.getIncrement());
141        });
142    }
143
144    public Optional<Long> getNumberValue() {
145        parse();
146
147        fieldDefinition.getFieldNumber().ifPresent(fieldNumber -> checkState(numberIndex.size() > fieldNumber,
148            "Only %s fields in %s, field %s requested.", numberElements.size(), print(), fieldNumber));
149
150        return value();
151    }
152
153    @Override
154    public String toString() {
155        return new StringJoiner(", ", NumberField.class.getSimpleName() + "[", "]")
156            .add("valueProvider=" + valueProvider)
157            .add("numberElements=" + numberElements)
158            .add("numberIndex=" + numberIndex)
159            .add("fieldDefinition=" + fieldDefinition)
160            .toString();
161    }
162
163    private static final class NumberElement {
164
165        private final boolean number;
166        private String value;
167
168        private NumberElement(boolean number, String value) {
169            this.number = number;
170            this.value = value;
171        }
172
173        String getFieldValue() {
174            return value;
175        }
176
177        void setLongValue(long value) {
178            this.value = Long.toString(value);
179        }
180
181        Optional<Long> getLongValue() {
182            return number ? Optional.of(Long.parseLong(value)) : Optional.empty();
183        }
184
185        @Override
186        public String toString() {
187            return new StringJoiner(", ", NumberElement.class.getSimpleName() + "[", "]")
188                .add("number=" + number)
189                .add("value='" + value + "'")
190                .toString();
191        }
192    }
193}