001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.tools.formatting.template;
017
018import static de.cuioss.tools.base.Preconditions.checkState;
019import static java.util.Objects.requireNonNull;
020
021import java.util.ArrayList;
022import java.util.List;
023
024import de.cuioss.tools.formatting.template.lexer.Lexer;
025import de.cuioss.tools.formatting.template.lexer.LexerBuilder;
026import de.cuioss.tools.formatting.template.token.Token;
027import lombok.EqualsAndHashCode;
028import lombok.Synchronized;
029import lombok.ToString;
030
031/**
032 * Formatter which is able to replace parameter inside the template based on
033 * {@link FormatterSupport} information. See {@link de.cuioss.tools.formatting}
034 * for details.
035 *
036 * @param <T> at least {@link FormatterSupport}
037 *
038 * @author Eugen Fischer
039 */
040@ToString
041@EqualsAndHashCode
042public final class TemplateFormatterImpl<T extends FormatterSupport> implements TemplateFormatter<T> {
043
044    private static final long serialVersionUID = -6297959581838201331L;
045
046    private final String template;
047
048    private ArrayList<Token> parsedTokens;
049
050    private Lexer<T> lexer;
051
052    private final boolean strict;
053
054    private TemplateFormatterImpl(final String template, final boolean strict) {
055        this.template = template;
056        this.strict = strict;
057    }
058
059    /**
060     * Lexer which should be used to scan the template
061     *
062     * @param lexerInstance must not be {@code null}
063     *
064     * @return reference to TemplateFormatter, fluent api style
065     */
066    TemplateFormatterImpl<T> scanBy(final Lexer<T> lexerInstance) {
067        lexer = requireNonNull(lexerInstance, "Parser must not be null");
068        parsedTokens = null;
069        return this;
070    }
071
072    /**
073     * replace attributes from template by attribute values from the map. missing
074     * template attributes will be ignored and doesn't add to result at all.
075     *
076     * @param reference must not be null
077     *
078     * @return completed template
079     */
080    @Override
081    public String format(final T reference) {
082
083        requireNonNull(reference, "Reference must not be null");
084
085        final var tokenList = getParsedTokens();
086
087        final var buffer = new StringBuilder(0);
088
089        for (var index = 0; index < tokenList.size(); index++) {
090            final var token = tokenList.get(index);
091            if (token.isStringToken()) {
092                if (lookUpLastTokenHasValue(tokenList, reference, index)
093                        && lookUpNextTokenHasValue(tokenList, reference, index)) {
094                    buffer.append(token.substituteAttribute(reference));
095                }
096            } else {
097                buffer.append(token.substituteAttribute(reference));
098            }
099        }
100
101        return buffer.toString();
102    }
103
104    private boolean lookUpNextTokenHasValue(final List<Token> tokenList, final T reference,
105            final int currentTokenIndex) {
106
107        var nextTokenIndex = currentTokenIndex + 1;
108        while (nextTokenIndex < tokenList.size()) {
109            final var token = tokenList.get(nextTokenIndex);
110            if (!token.isStringToken()) {
111                final var value = token.substituteAttribute(reference);
112                if (!value.isEmpty()) {
113                    return true;
114                }
115            }
116            nextTokenIndex++;
117        }
118        return false;
119    }
120
121    private boolean lookUpLastTokenHasValue(final List<Token> tokenList, final T reference,
122            final int currentTokenIndex) {
123
124        var result = false;
125        final var lastTokenIndex = currentTokenIndex - 1;
126        if (lastTokenIndex >= 0) {
127            final var value = tokenList.get(lastTokenIndex).substituteAttribute(reference);
128            result = !value.isEmpty();
129        }
130        return result;
131    }
132
133    @Synchronized
134    private List<Token> getParsedTokens() {
135        if (null == this.parsedTokens) {
136            checkState(null != this.lexer, "Parser must be initialized before.");
137            this.parsedTokens = new ArrayList<>(this.lexer.scan(this.template));
138        }
139        return this.parsedTokens;
140    }
141
142    /**
143     * The created TemplateFormatter provide only usage of simple expression
144     * language with squared brackets.
145     *
146     * @param template   must not be null
147     * @param sourceType must not be null
148     *
149     * @return TemplateFormatter which using template and lexer which was forwarded
150     */
151    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
152            final Class<F> sourceType) {
153        return TemplateBuilder.useTemplate(template).forType(sourceType);
154    }
155
156    /**
157     * @param template must not be null
158     * @param source   must not be null
159     *
160     * @return TemplateFormatter
161     */
162    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
163            final F source) {
164        return TemplateBuilder.useTemplate(template).forSource(source);
165    }
166
167    /**
168     * @param template must not be null
169     * @param lexer    must not be null
170     *
171     * @return TemplateFormatter which using template and lexer which was forwarded
172     */
173    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
174            final Lexer<F> lexer) {
175        return TemplateBuilder.useTemplate(template).scanBy(lexer);
176    }
177
178    /**
179     * The created TemplateFormatter provide only usage of simple expression
180     * language with squared brackets.
181     *
182     * @param template   must not be null
183     * @param sourceType must not be null
184     * @param strict     use strict mode for pattern matching (only match exact
185     *                   name) instead of best fitting
186     *
187     * @return TemplateFormatter which using template and lexer which was forwarded
188     */
189    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
190            final Class<F> sourceType, final boolean strict) {
191        return TemplateBuilder.useTemplate(template).strict(strict).forType(sourceType);
192    }
193
194    /**
195     * @param template must not be null
196     * @param source   must not be null
197     * @param strict   use strict mode for pattern matching (only match exact name)
198     *                 instead of best fitting
199     *
200     * @return TemplateFormatter
201     */
202    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
203            final F source, final boolean strict) {
204        return TemplateBuilder.useTemplate(template).strict(strict).forSource(source);
205    }
206
207    /**
208     * @param template must not be null
209     * @param lexer    must not be null
210     * @param strict   use strict mode for pattern matching (only match exact name)
211     *                 instead of best fitting
212     *
213     * @return TemplateFormatter which using template and lexer which was forwarded
214     */
215    public static <F extends FormatterSupport> TemplateFormatter<F> createFormatter(final String template,
216            final Lexer<F> lexer, final boolean strict) {
217        return TemplateBuilder.useTemplate(template).strict(strict).scanBy(lexer);
218    }
219
220    /**
221     * @return a newly created {@link TemplateBuilder}
222     */
223    @SuppressWarnings("squid:S2440") // owolff: False positive
224    public static TemplateBuilder builder() {
225        return new TemplateBuilder();
226    }
227
228    /**
229     * Template Builder
230     *
231     * @author Eugen Fischer
232     */
233    public static final class TemplateBuilder {
234
235        private TemplateBuilder() {
236        }
237
238        static FormatterBuilder useTemplate(final String template) {
239            return new FormatterBuilder(template);
240        }
241
242        static final class FormatterBuilder {
243
244            private final String template;
245
246            private boolean strict;
247
248            FormatterBuilder(final String templateInput) {
249                template = templateInput;
250            }
251
252            /**
253             * @param strict use strict mode for pattern matching (only match exact name)
254             *               instead of best fitting
255             * @return a {@link FormatterBuilder} with strict set to given parameter
256             */
257            public FormatterBuilder strict(boolean strict) {
258                this.strict = strict;
259                return this;
260            }
261
262            public <F extends FormatterSupport> TemplateFormatter<F> scanBy(final Lexer<F> lexer) {
263                return new TemplateFormatterImpl<F>(template, strict).scanBy(lexer);
264            }
265
266            public <F extends FormatterSupport> TemplateFormatter<F> forSource(final F source) {
267                final Lexer<F> lexer = LexerBuilder.useSimpleElWithSquaredBrackets().strict(strict).build(source);
268                return scanBy(lexer);
269            }
270
271            public <F extends FormatterSupport> TemplateFormatter<F> forType(final Class<F> classType) {
272                final Lexer<F> lexer = LexerBuilder.useSimpleElWithSquaredBrackets().strict(strict).build(classType);
273                return scanBy(lexer);
274            }
275        }
276    }
277}