All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.microprofile.graphql.server.FormattingHelper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
 *
 * 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 io.helidon.microprofile.graphql.server;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;

import jakarta.json.bind.annotation.JsonbDateFormat;
import jakarta.json.bind.annotation.JsonbNumberFormat;
import org.eclipse.microprofile.graphql.DateFormat;

import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_DECIMAL_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BIG_INTEGER_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.BYTE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DEFAULT_LOCALE;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DOUBLE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DOUBLE_PRIMITIVE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FLOAT_PRIMITIVE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INT;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INTEGER_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.INTEGER_PRIMITIVE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LEGACY_DATE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_DATE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_DATE_TIME_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LOCAL_TIME_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LONG_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.LONG_PRIMITIVE_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.OFFSET_DATE_TIME_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.OFFSET_TIME_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.SHORT_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.SUPPORTED_SCALARS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ZONED_DATE_TIME_CLASS;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureRuntimeException;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getAnnotationValue;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldAnnotations;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodAnnotations;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getParameterAnnotations;

/**
 * Helper class for number formatting.
 */
class FormattingHelper {

    /**
     * Logger.
     */
    private static final System.Logger LOGGER = System.getLogger(FormattingHelper.class.getName());

    /**
     * Defines no default format.
     */
    private static final String[] NO_DEFAULT_FORMAT = new String[0];

    /**
     * Indicates date formatting applied.
     */
    protected static final String DATE = "Date";

    /**
     * Indicates number formatting applied.
     */
    protected static final String NUMBER = "Number";

    /**
     * Indicates no formatting applied.
     */
    static final String[] NO_FORMATTING = new String[] {null, null, null };

    /**
     * No-args constructor.
     */
    private FormattingHelper() {
    }

    /**
     * Return the default date/time format for the given class.
     *
     * @param scalarName scalar to check
     * @param clazzName  class name to validate against
     * @return he default date/time format for the given class
     */
    protected static String[] getDefaultDateTimeFormat(String scalarName, String clazzName) {
        for (SchemaScalar scalar : SUPPORTED_SCALARS.values()) {
            if (scalarName.equals(scalar.name()) && scalar.actualClass().equals(clazzName)) {
                return new String[] {scalar.defaultFormat(), DEFAULT_LOCALE };
            }
        }
        return NO_DEFAULT_FORMAT;
    }

    /**
     * Return a {@link NumberFormat} for the given type, locale and format.
     *
     * @param type   the GraphQL type or scalar
     * @param locale the locale, either "" or the correct locale
     * @param format the format to use, may be null
     * @return The correct {@link NumberFormat} for the given type and locale
     */
    protected static NumberFormat getCorrectNumberFormat(String type, String locale, String format) {
        Locale actualLocale = DEFAULT_LOCALE.equals(locale)
                ? Locale.getDefault()
                : Locale.forLanguageTag(locale);
        NumberFormat numberFormat;
        if (FLOAT.equals(type)
                || BIG_DECIMAL.equals(type)
                || BIG_DECIMAL_CLASS.equals(type)
                || FLOAT_CLASS.equals(type)
                || FLOAT_PRIMITIVE_CLASS.equals(type)
                || DOUBLE_CLASS.equals(type)
                || DOUBLE_PRIMITIVE_CLASS.equals(type)) {
            numberFormat = NumberFormat.getNumberInstance(actualLocale);
        } else if (INT.equals(type)
                || INTEGER_CLASS.equals(type)
                || INTEGER_PRIMITIVE_CLASS.equals(type)
                || BIG_INTEGER_CLASS.equals(type)
                || BYTE_CLASS.equals(type)
                || SHORT_CLASS.equals(type)
                || LONG_CLASS.equals(type)
                || LONG_PRIMITIVE_CLASS.equals(type)
                || BIG_INTEGER.equals(type)) {
            numberFormat = NumberFormat.getIntegerInstance(actualLocale);
        } else {
            return null;
        }
        if (format != null && !format.trim().equals("")) {
            ((DecimalFormat) numberFormat).applyPattern(format);
        }
        return numberFormat;
    }

    /**
     * Return a {@link DateTimeFormatter} for the given type, locale and format.
     *
     * @param type   the GraphQL type or scalar
     * @param locale the locale, either "" or the correct locale
     * @param format the format to use, may be null
     * @return The correct {@link java.text.DateFormat } for the given type and locale
     */
    protected static DateTimeFormatter getCorrectDateFormatter(String type, String locale, String format) {
        Locale actualLocale = DEFAULT_LOCALE.equals(locale)
                ? Locale.getDefault()
                : Locale.forLanguageTag(locale);

        DateTimeFormatter formatter;

        if (format != null && !LEGACY_DATE_CLASS.equals(type)) {
            formatter = DateTimeFormatter.ofPattern(format, actualLocale);
        } else {
            // handle defaults if no format specified
            if (OFFSET_TIME_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_OFFSET_TIME.withLocale(actualLocale);
            } else if (LOCAL_TIME_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_LOCAL_TIME.withLocale(actualLocale);
            } else if (OFFSET_DATE_TIME_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(actualLocale);
            } else if (ZONED_DATE_TIME_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale(actualLocale);
            } else if (LOCAL_DATE_TIME_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(actualLocale);
            } else if (LOCAL_DATE_CLASS.equals(type)) {
                formatter = DateTimeFormatter.ISO_LOCAL_DATE.withLocale(actualLocale);
            } else {
                return null;
            }
        }
        return formatter;
    }

    /**
     * Return a {@link NumberFormat} for the given type and locale.
     *
     * @param type   the GraphQL type or scalar
     * @param locale the locale, either "" or the correct locale
     * @return The correct {@link NumberFormat} for the given type and locale
     */
    protected static NumberFormat getCorrectNumberFormat(String type, String locale) {
        return getCorrectNumberFormat(type, locale, null);
    }

    /**
     * Return formatting annotation details for the {@link AnnotatedElement}. The array returned contains three elements: [0] =
     * either DATE, NUMBER or null if no formatting applied: [1][2] = if formatting applied then the format and locale.
     *
     * @param annotatedElement the {@link AnnotatedElement} to check
     * @return formatting annotation details or array with three nulls if no formatting
     */
    protected static String[] getFormattingAnnotation(AnnotatedElement annotatedElement) {
        String[] dateFormat = getDateFormatAnnotation(annotatedElement);
        String[] numberFormat = getNumberFormatAnnotation(annotatedElement);

        if (dateFormat.length == 0 && numberFormat.length == 0) {
            return NO_FORMATTING;
        }
        if (dateFormat.length == 2 && numberFormat.length == 2) {
            ensureRuntimeException(LOGGER, "A date format and number format cannot be applied to the same element: "
                    + annotatedElement);
        }

        return dateFormat.length == 2
                ? new String[] {DATE, dateFormat[0], dateFormat[1] }
                : new String[] {NUMBER, numberFormat[0], numberFormat[1] };
    }

    /**
     * Return the field format with the given index.
     *
     * @param field {@link Field} to introspect
     * @param index index of generic type. 0 = List/Collection, 1 = Map
     * @return the field format with the given index
     */
    protected static String[] getFieldFormat(Field field, int index) {
        Annotation[] annotations = getFieldAnnotations(field, index);
        if (annotations == null || annotations.length == 0) {
            // try to get standard parameter annotation.
            annotations = field.getAnnotatedType().getAnnotations();
        }

        return getFormatFromAnnotations(annotations);
    }

    /**
     * Return the method format with the given index.
     *
     * @param method {@link Method} to introspect
     * @param index     index of generic type. 0 = List/Collection, 1 = Map
     * @return the method format with the given index
     */
    protected static String[] getMethodFormat(Method method, int index) {
        Annotation[] annotations = getMethodAnnotations(method, index);
        if (annotations == null || annotations.length == 0) {
            // try to get standard parameter annotation.
            annotations = method.getAnnotatedReturnType().getAnnotations();
        }
        return getFormatFromAnnotations(annotations);
    }

    /**
     * Return the method parameter format with the given index.
     *
     * @param parameter {@link Parameter} to introspect
     * @param index     index of generic type. 0 = List/Collection, 1 = Map
     * @return the method parameter format with the given index
     */
    protected static String[] getMethodParameterFormat(Parameter parameter, int index) {
        Annotation[] annotations = getParameterAnnotations(parameter, index);
        if (annotations == null || annotations.length == 0) {
            // try to get standard parameter annotation.
            annotations = parameter.getAnnotatedType().getAnnotations();
        }

        return getFormatFromAnnotations(annotations);
    }

    /**
     * Return the formatting from the given annotations.
     *
     * @param annotations {@link Annotation}s to retrieve formatting from
     * @return the formatting from the given annotations
     */
    protected static String[] getFormatFromAnnotations(Annotation[] annotations) {
        if (annotations != null && annotations.length > 0) {
            JsonbDateFormat dateFormat1 = (JsonbDateFormat) getAnnotationValue(annotations, JsonbDateFormat.class);
            DateFormat dateFormat2 = (DateFormat) getAnnotationValue(annotations, DateFormat.class);
            JsonbNumberFormat numberFormat1 = (JsonbNumberFormat) getAnnotationValue(annotations, JsonbNumberFormat.class);
            org.eclipse.microprofile.graphql.NumberFormat numberFormat2 =
                    (org.eclipse.microprofile.graphql.NumberFormat)
                            getAnnotationValue(annotations, org.eclipse.microprofile.graphql.NumberFormat.class);

            // ensure that both date and number formatting are not present
            if ((dateFormat1 != null || dateFormat2 != null)
                    && (numberFormat1 != null || numberFormat2 != null)) {
                ensureRuntimeException(LOGGER, "Cannot have date and number formatting on the same method");
            }
            String format = null;
            String locale = null;
            if (dateFormat1 != null) {
                format = dateFormat1.value();
                locale = dateFormat1.locale();
            } else if (dateFormat2 != null) {
                format = dateFormat2.value();
                locale = dateFormat2.locale();
            } else if (numberFormat1 != null) {
                format = numberFormat1.value();
                locale = numberFormat1.locale();
            } else if (numberFormat2 != null) {
                format = numberFormat2.value();
                locale = numberFormat2.locale();
            }

            if (format == null) {
                return NO_FORMATTING;
            }

            String type = dateFormat1 != null || dateFormat2 != null ? DATE
                    : NUMBER;

            return new String[] {type, format, locale.equals("") ? DEFAULT_LOCALE : locale };

        }
        return NO_FORMATTING;
    }

    /**
     * Indicates if either {@link JsonbNumberFormat} or {@link JsonbDateFormat} are present.
     *
     * @param annotatedElement the {@link AnnotatedElement} to check
     * @return true if either {@link JsonbNumberFormat} or {@link JsonbDateFormat} are present
     */
    protected static boolean isJsonbAnnotationPresent(AnnotatedElement annotatedElement) {
        return annotatedElement.getAnnotation(JsonbDateFormat.class) != null
                || annotatedElement.getAnnotation(JsonbNumberFormat.class) != null;
    }

    /**
     * Return the number format and locale for an {@link AnnotatedElement} if they exist in a {@link String} array.
     *
     * @param annotatedElement the {@link AnnotatedElement} to check
     * @return the format ([0]) and locale ([1]) for a parameter in a {@link String} array or an empty array if not
     */
    protected static String[] getNumberFormatAnnotation(AnnotatedElement annotatedElement) {
        return getNumberFormatAnnotationInternal(
                annotatedElement.getAnnotation(JsonbNumberFormat.class),
                annotatedElement.getAnnotation(org.eclipse.microprofile.graphql.NumberFormat.class));
    }

    /**
     * Return the number format and locale for the given annotations.
     *
     * @param jsonbNumberFormat {@link JsonbNumberFormat} annotation, may be null
     * @param numberFormat      {@link NumberFormat} annotation, may be none
     * @return the format ([0]) and locale ([1]) for a method in a {@link String} array or an empty array if not
     */
    private static String[] getNumberFormatAnnotationInternal(JsonbNumberFormat jsonbNumberFormat,
                                                              org.eclipse.microprofile.graphql.NumberFormat numberFormat) {
        // check @NumberFormat first as this takes precedence
        if (numberFormat != null) {
            return new String[] {numberFormat.value(), numberFormat.locale() };
        }
        if (jsonbNumberFormat != null) {
            return new String[] {jsonbNumberFormat.value(), jsonbNumberFormat.locale() };
        }
        return new String[0];
    }

    /**
     * Return the date format and locale for a field if they exist in a {@link String} array.
     *
     * @param annotatedElement the {@link AnnotatedElement} to check
     * @return the format ([0]) and locale ([1])  for a field in a {@link String} array or an empty array if not
     */
    protected static String[] getDateFormatAnnotation(AnnotatedElement annotatedElement) {
        return getDateFormatAnnotationInternal(
                annotatedElement.getAnnotation(JsonbDateFormat.class),
                annotatedElement.getAnnotation(org.eclipse.microprofile.graphql.DateFormat.class));
    }

    /**
     * Return the date format and locale for the given annotations.
     *
     * @param jsonbDateFormat {@link JsonbDateFormat} annotation, may be null
     * @param dateFormat      {@link DateFormat} annotation, may be none
     * @return the format ([0]) and locale ([1]) for a method in a {@link String} array or an empty array if not
     */
    private static String[] getDateFormatAnnotationInternal(JsonbDateFormat jsonbDateFormat,
                                                            DateFormat dateFormat) {
        // check @DateFormat first as this takes precedence
        if (dateFormat != null) {
            return new String[] {dateFormat.value(), dateFormat.locale() };
        }
        if (jsonbDateFormat != null) {
            return new String[] {jsonbDateFormat.value(), jsonbDateFormat.locale() };
        }
        return new String[0];
    }

    /**
     * Format a date value given a {@link DateTimeFormatter}.
     *
     * @param originalResult    original result
     * @param dateTimeFormatter {@link DateTimeFormatter} to format with
     * @return formatted value
     */
    @SuppressWarnings({"unchecked", "rawtypes" })
    public static Object formatDate(Object originalResult, DateTimeFormatter dateTimeFormatter) {
        if (originalResult == null) {
            return null;
        }
        if (originalResult instanceof Collection) {
            Collection formattedResult = new ArrayList();
            Collection originalCollection = (Collection) originalResult;
            originalCollection.forEach(e -> formattedResult.add(e instanceof TemporalAccessor ? dateTimeFormatter
                    .format((TemporalAccessor) e) : e)
            );
            return formattedResult;
        } else {
            return originalResult instanceof TemporalAccessor
                    ? dateTimeFormatter.format((TemporalAccessor) originalResult) : originalResult;
        }
    }

    /**
     * Format a date value given a {@link SimpleDateFormat} for a {@link Date}.
     *
     * @param originalResult   original result
     * @param simpleDateFormat {@link SimpleDateFormat} to format with
     * @return formatted value
     */
    @SuppressWarnings({"unchecked", "rawtypes" })
    public static Object formatDate(Object originalResult, SimpleDateFormat simpleDateFormat) {
        if (originalResult == null) {
            return null;
        }
        if (originalResult instanceof Collection) {
            Collection formattedResult = new ArrayList();
            Collection originalCollection = (Collection) originalResult;
            originalCollection.forEach(e -> formattedResult.add(e instanceof DateTimeFormatter ? simpleDateFormat
                    .format(e) : e)
            );
            return formattedResult;
        } else {
            return originalResult instanceof Date
                    ? simpleDateFormat.format(originalResult) : originalResult;
        }
    }

    /**
     * Format a number value with a a given {@link NumberFormat}.
     *
     * @param originalResult original result
     * @param isScalar       indicates if it is a scalar value
     * @param numberFormat   {@link NumberFormat} to format with
     * @return formatted value
     */
    @SuppressWarnings({"unchecked", "rawtypes" })
    public static Object formatNumber(Object originalResult, boolean isScalar, NumberFormat numberFormat) {
        if (originalResult == null) {
            return null;
        }
        if (originalResult instanceof Collection) {
            Collection formattedResult = new ArrayList();
            Collection originalCollection = (Collection) originalResult;
            originalCollection.forEach(e -> formattedResult.add(numberFormat.format(e)));
            return formattedResult;
        }
        return numberFormat.format(originalResult);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy