/** * Copyright 2011-2017 Asakusa Framework Team. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.asakusafw.dmdl.directio.text; import static com.asakusafw.dmdl.directio.text.TextFormatConstants.*; import java.nio.charset.Charset; import java.time.DateTimeException; import java.time.ZoneId; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.dmdl.Diagnostic; import com.asakusafw.dmdl.directio.util.ClassName; import com.asakusafw.dmdl.directio.util.DatePattern; import com.asakusafw.dmdl.directio.util.DecimalPattern; import com.asakusafw.dmdl.directio.util.MapValue; import com.asakusafw.dmdl.directio.util.Value; import com.asakusafw.dmdl.model.AstAttribute; import com.asakusafw.dmdl.model.AstAttributeElement; import com.asakusafw.dmdl.model.AstAttributeValue; import com.asakusafw.dmdl.model.AstAttributeValueArray; import com.asakusafw.dmdl.model.AstAttributeValueMap; import com.asakusafw.dmdl.model.AstLiteral; import com.asakusafw.dmdl.model.AstNode; import com.asakusafw.dmdl.model.AstSimpleName; import com.asakusafw.dmdl.model.LiteralKind; import com.asakusafw.dmdl.semantics.DmdlSemantics; /** * Analyzes DMDL attributes. * @since 0.9.1 */ public class AttributeAnalyzer { static final Logger LOG = LoggerFactory.getLogger(AttributeAnalyzer.class); private final DmdlSemantics environment; private final AstAttribute attribute; private boolean sawError = false; /** * Creates a new instance. * @param environment the current environment * @param attribute the source attribute */ public AttributeAnalyzer(DmdlSemantics environment, AstAttribute attribute) { this.environment = environment; this.attribute = attribute; } /** * Whether or not errors were occurred. * @return {@code true} if occurred */ public boolean hasError() { return sawError; } /** * Analyze the given element as a string value. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<String> toString(AstAttributeElement element) { return parseString(element.value) .map(s -> Value.of(element, s)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotString")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a nullable string value. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<String> toStringWithNull(AstAttributeElement element) { if (isName(element.value, VALUE_NULL)) { return Value.of(element, null); } return parseStringLiteral(element.value) .map(s -> Value.of(element, s)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotString")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a boolean value. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<Boolean> toBoolean(AstAttributeElement element) { return parseString(element.value) .map(s -> s.toLowerCase(Locale.ENGLISH)) .flatMap(s -> { if (s.equals(VALUE_TRUE)) { return Optional.of(true); } else if (s.equals(VALUE_FALSE)) { return Optional.of(false); } else { return Optional.empty(); } }) .map(b -> Value.of(element, b)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotBoolean")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a character value. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<Character> toCharacter(AstAttributeElement element) { return parseStringLiteral(element.value) .filter(s -> s.length() == 1) .map(s -> Value.of(element, s.charAt(0))) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotCharacter")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a character set. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<Charset> toCharset(AstAttributeElement element) { return parseString(element.value) .flatMap(s -> { try { return Optional.of(Charset.forName(s)); } catch (IllegalArgumentException e) { LOG.trace("invalid charset: {}", s, e); //$NON-NLS-1$ return Optional.empty(); } }) .map(cs -> Value.of(element, cs)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotCharsetName")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a decimal format. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<DecimalPattern> toDecimalPatternWithNull(AstAttributeElement element) { if (isName(element.value, VALUE_NULL)) { return Value.of(element, null); } return parseStringLiteral(element.value) .filter(DecimalPattern::isValid) .map(s -> Value.of(element, new DecimalPattern(s))) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotDecimalFormat")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a date format. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<DatePattern> toDatePattern(AstAttributeElement element) { return parseStringLiteral(element.value) .filter(DatePattern::isValid) .map(s -> Value.of(element, new DatePattern(s))) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotDateFormat")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a time-zone ID. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<ZoneId> toZoneIdWithNull(AstAttributeElement element) { if (isName(element.value, VALUE_NULL)) { return Value.of(element, null); } return parseStringLiteral(element.value) .flatMap(s -> { try { return Optional.of(ZoneId.of(s)); } catch (DateTimeException e) { LOG.trace("invalid time zone: {}", s, e); //$NON-NLS-1$ return Optional.empty(); } }) .map(v -> Value.of(element, v)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotTimeZoneId")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a class name. * @param element the target element * @return the analyzed value, or undefined if the element value is not valid */ public Value<ClassName> toClassName(AstAttributeElement element) { return toClassName(element, Optional::of); } /** * Analyze the given element as an enum constant. * @param <T> the enum type * @param element the target element * @param type the enum type * @return the analyzed value, or undefined if the element value is not valid */ public <T extends Enum<T>> Value<T> toEnumConstant(AstAttributeElement element, Class<T> type) { return parseString(element.value) .flatMap(s -> { try { return Optional.of(Enum.valueOf(type, s.toUpperCase(Locale.ENGLISH))); } catch (IllegalArgumentException e) { LOG.trace("invalid enum constant: {}#{}", type.getSimpleName(), s, e); //$NON-NLS-1$ return Optional.empty(); } }) .map(v -> Value.of(element, v)) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotEnumConstant"), //$NON-NLS-1$ Arrays.stream(type.getEnumConstants()) .map(c -> c.name().toLowerCase(Locale.ENGLISH)) .collect(Collectors.joining(", "))); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a class name. * @param element the target element * @param resolver the resolver * @return the analyzed value, or undefined if the element value is not valid */ public Value<ClassName> toClassName(AstAttributeElement element, Function<String, Optional<String>> resolver) { if (isName(element.value, VALUE_NULL)) { return Value.of(element, null); } return parseString(element.value) .flatMap(resolver) .filter(ClassName::isValid) .map(s -> Value.of(element, new ClassName(s))) .orElseGet(() -> { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotClassName")); //$NON-NLS-1$ return Value.undefined(); }); } /** * Analyze the given element as a character map. * @param element the target element * @return the analyzed map, or undefined if the element value is not valid */ public MapValue<Character, Character> toCharacterMapWithNullValue(AstAttributeElement element) { if (element.value instanceof AstAttributeValueArray && ((AstAttributeValueArray) element.value).elements.isEmpty()) { return new MapValue<>(null); } if (!(element.value instanceof AstAttributeValueMap)) { error(element, Messages.getString("AttributeAnalyzer.diagnosticNotMap")); //$NON-NLS-1$ return new MapValue<>(null); } MapValue<Character, Character> results = new MapValue<>(element); for (AstAttributeValueMap.Entry entry : ((AstAttributeValueMap) element.value).entries) { Character key = parseStringLiteral(entry.key) .filter(s -> s.length() == 1) .map(s -> s.charAt(0)) .orElseGet(() -> { error(element, entry, Messages.getString("AttributeAnalyzer.diagnosticMapKeyNotCharacter")); //$NON-NLS-1$ return null; }); Character value = parseStringLiteral(entry.value) .filter(s -> s.length() == 1) .map(s -> s.charAt(0)) .orElseGet(() -> { if (isName(entry.value, TextFormatConstants.VALUE_NULL) == false) { error(element, entry, Messages.getString("AttributeAnalyzer.diagnosticMapValueNotCharacter")); //$NON-NLS-1$ } return null; }); results.add(entry, key, value); } return results; } /** * Reports an error. * @param element the target element * @param message the error message pattern, <code>{0}</code> is reserved for the element path * @param arguments the pattern arguments */ public void error(AstAttributeElement element, String message, Object... arguments) { report(Diagnostic.Level.ERROR, element.name, message, toArgs(element, arguments)); } /** * Reports an error. * @param element the target element * @param entry the map entry * @param message the error message pattern, <code>{0}</code> is reserved for the element path * @param arguments the pattern arguments */ public void error( AstAttributeElement element, AstAttributeValueMap.Entry entry, String message, Object... arguments) { report(Diagnostic.Level.ERROR, entry.key, message, toArgs(element, entry, arguments)); } /** * Reports a warning. * @param element the target element * @param message the error message pattern, <code>{0}</code> is reserved for the element path * @param arguments the pattern arguments */ public void warn(AstAttributeElement element, String message, Object... arguments) { report(Diagnostic.Level.WARN, element.name, message, toArgs(element, arguments)); } /** * Reports a warning. * @param element the target element * @param entry the map entry * @param message the error message pattern, <code>{0}</code> is reserved for the element path * @param arguments the pattern arguments */ public void warn( AstAttributeElement element, AstAttributeValueMap.Entry entry, String message, Object... arguments) { report(Diagnostic.Level.WARN, entry.key, message, toArgs(element, entry, arguments)); } private Object[] toArgs(AstAttributeElement element, Object... args) { Object[] arguments = new Object[args.length + 1]; arguments[0] = String.format("@%s(%s)", //$NON-NLS-1$ attribute.name.toString(), element.name.identifier); System.arraycopy(args, 0, arguments, 1, args.length); return arguments; } private Object[] toArgs(AstAttributeElement element, AstAttributeValueMap.Entry entry, Object... args) { Object[] arguments = new Object[args.length + 1]; arguments[0] = String.format("@%s(%s[%s])", //$NON-NLS-1$ attribute.name.toString(), element.name.identifier, entry.key.token); System.arraycopy(args, 0, arguments, 1, args.length); return arguments; } private void report(Diagnostic.Level level, AstNode node, String message, Object[] arguments) { sawError |= level == Diagnostic.Level.ERROR; environment.report(new Diagnostic(level, node, message, arguments)); } private static Optional<String> parseString(AstAttributeValue value) { return first(value, AttributeAnalyzer::parseStringLiteral, AttributeAnalyzer::bareLiteral, AttributeAnalyzer::parseSimpleName); } private static Optional<String> parseStringLiteral(AstAttributeValue value) { if (value instanceof AstLiteral) { AstLiteral literal = (AstLiteral) value; if (literal.getKind() == LiteralKind.STRING) { return Optional.of(literal.toStringValue()); } } return Optional.empty(); } private static Optional<String> bareLiteral(AstAttributeValue value) { if (value instanceof AstLiteral) { return Optional.of(((AstLiteral) value).getToken()); } return Optional.empty(); } private static Optional<String> parseSimpleName(AstAttributeValue value) { if (value instanceof AstSimpleName) { return Optional.of(((AstSimpleName) value).identifier); } return Optional.empty(); } private static boolean isName(AstAttributeValue value, String constant) { return parseSimpleName(value) .filter(Predicate.isEqual(constant)) .isPresent(); } @SafeVarargs private static <K, V> Optional<V> first(K input, Function<? super K, Optional<V>>... mappers) { for (Function<? super K, Optional<V>> mapper : mappers) { Optional<V> value = mapper.apply(input); if (value.isPresent()) { return value; } } return Optional.empty(); } }