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}