org.phenotips.measurements.internal.DefaultMeasurementsChartConfigurationsFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.measurements.internal.DefaultMeasurementsChartConfigurationsFactory.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.measurements.internal;

import org.phenotips.measurements.MeasurementsChartConfiguration;
import org.phenotips.measurements.MeasurementsChartConfigurationsFactory;

import org.xwiki.component.annotation.Component;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle;

import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

/**
 * Implementation of {@link MeasurementsChartConfigurationsFactory} that uses a {@link ResourceBundle .properties} file
 * with the chart configurations.
 *
 * @version $Id: b0f3c156b8ca285f6675213c8dbd225998c8bc43 $
 * @since 1.0M3
 */
@Component
public class DefaultMeasurementsChartConfigurationsFactory implements MeasurementsChartConfigurationsFactory {
    /** Logging helper object. */
    @Inject
    private Logger logger;

    /**
     * Simple read-only bean implementation of {@link MeasurementsChartConfiguration}, populated by
     * {@link DefaultMeasurementsChartConfigurationsFactory}.
     */
    private static final class SimpleMeasurementsChartConfiguration implements MeasurementsChartConfiguration {
        /** Internal identifier used for debugging. Same as the prefix used in the configuration file. */
        private final String chartIdentifier;

        /** @see #getMeasurementType() */
        private String measurementType;

        /** @see #getLowerAgeLimit() */
        private int lowerAgeLimit;

        /** @see #getUpperAgeLimit() */
        private int upperAgeLimit = 240;

        /** @see #getAgeTickStep() */
        private int ageTickStep = 6;

        /** @see #getAgeLabelStep() */
        private int ageLabelStep = 12;

        /** @see #getLowerValueLimit() */
        private double lowerValueLimit;

        /** @see #getUpperValueLimit() */
        private double upperValueLimit;

        /** @see #getValueTickStep() */
        private double valueTickStep = 0.2;

        /** @see #getValueLabelStep() */
        private double valueLabelStep = 1;

        /** @see #getChartTitle() */
        private String chartTitle;

        /** @see #getTopLabel() */
        private String topLabel;

        /** @see #getBottomLabel() */
        private String bottomLabel;

        /** @see #getLeftLabel() */
        private String leftLabel;

        /** @see #getRightLabel() */
        private String rightLabel;

        /** @see #getChartSource() */
        private String chartSource;

        /** @see #getChartSourceLink() */
        private URL chartSourceLink;

        /**
         * Private constructor.
         *
         * @param identifier the chart identifier used, see {@link #chartIdentifier}
         */
        private SimpleMeasurementsChartConfiguration(String identifier) {
            this.chartIdentifier = identifier;
        }

        @Override
        public String getMeasurementType() {
            return this.measurementType;
        }

        @Override
        public int getLowerAgeLimit() {
            return this.lowerAgeLimit;
        }

        @Override
        public int getUpperAgeLimit() {
            return this.upperAgeLimit;
        }

        @Override
        public int getAgeTickStep() {
            return this.ageTickStep;
        }

        @Override
        public int getAgeLabelStep() {
            return this.ageLabelStep;
        }

        @Override
        public double getLowerValueLimit() {
            return this.lowerValueLimit;
        }

        @Override
        public double getUpperValueLimit() {
            return this.upperValueLimit;
        }

        @Override
        public double getValueTickStep() {
            return this.valueTickStep;
        }

        @Override
        public double getValueLabelStep() {
            return this.valueLabelStep;
        }

        @Override
        public String getChartTitle() {
            return StringUtils.defaultString(this.chartTitle);
        }

        @Override
        public String getTopLabel() {
            if (StringUtils.isEmpty(this.topLabel)) {
                return this.bottomLabel;
            }
            return this.topLabel;
        }

        @Override
        public String getBottomLabel() {
            return this.bottomLabel;
        }

        @Override
        public String getLeftLabel() {
            if (StringUtils.isEmpty(this.leftLabel)) {
                return this.rightLabel;
            }
            return this.leftLabel;
        }

        @Override
        public String getRightLabel() {
            if (StringUtils.isEmpty(this.rightLabel)) {
                return this.leftLabel;
            }
            return this.rightLabel;
        }

        @Override
        public String getChartSource() {
            return this.chartSource;
        }

        @Override
        public URL getChartSourceLink() {
            return this.chartSourceLink;
        }
    }

    @Override
    public List<MeasurementsChartConfiguration> loadConfigurationsForMeasurementType(String measurementType) {
        ResourceBundle configuration = ResourceBundle.getBundle("measurementsChartsConfigurations");
        String key = "charts." + measurementType + ".configurations";
        if (!configuration.containsKey(key)) {
            return Collections.emptyList();
        }
        String[] charts = configuration.getString(key).split(",");
        List<MeasurementsChartConfiguration> result = new ArrayList<>(charts.length);
        for (String chart : charts) {
            SimpleMeasurementsChartConfiguration chartSettings = loadChart(key + '.' + chart + '.', configuration);
            if (validateChart(chartSettings)) {
                chartSettings.measurementType = measurementType;
                result.add(chartSettings);
            }
        }
        return result;
    }

    /**
     * Load the settings for one chart.
     *
     * @param prefix the prefix for the configuration keys, in the format
     *            {@code charts.<measurement type>.configurations.<chart identifier>.}
     * @param configuration the resource bundle of the configuration
     * @return the configured settings
     */
    private SimpleMeasurementsChartConfiguration loadChart(String prefix, ResourceBundle configuration) {
        SimpleMeasurementsChartConfiguration result = new SimpleMeasurementsChartConfiguration(prefix);
        result.lowerAgeLimit = getIntSetting(prefix + "lowerAgeLimit", result.lowerAgeLimit, configuration);
        result.upperAgeLimit = getIntSetting(prefix + "upperAgeLimit", result.upperAgeLimit, configuration);
        result.ageTickStep = getIntSetting(prefix + "ageTickStep", result.ageTickStep, configuration);
        result.ageLabelStep = getIntSetting(prefix + "ageLabelStep", result.ageLabelStep, configuration);
        result.lowerValueLimit = getDoubleSetting(prefix + "lowerValueLimit", result.lowerValueLimit,
                configuration);
        result.upperValueLimit = getDoubleSetting(prefix + "upperValueLimit", result.upperValueLimit,
                configuration);
        result.valueTickStep = getPositiveDoubleSetting(prefix + "valueTickStep", result.valueTickStep,
                configuration);
        result.valueLabelStep = getPositiveDoubleSetting(prefix + "valueLabelStep", result.valueLabelStep,
                configuration);
        result.chartTitle = getStringSetting(prefix + "chartTitle", result.chartTitle, configuration);
        result.topLabel = getStringSetting(prefix + "topLabel", result.topLabel, configuration);
        result.bottomLabel = getStringSetting(prefix + "bottomLabel",
                StringUtils.defaultString(result.topLabel, "Age (years)"), configuration);
        result.leftLabel = getStringSetting(prefix + "leftLabel", result.leftLabel, configuration);
        result.rightLabel = getStringSetting(prefix + "rightLabel", result.rightLabel, configuration);
        result.chartSource = getStringSetting(prefix + "chartSource", result.chartSource, configuration);
        String url = getStringSetting(prefix + "chartSourceLink", null, configuration);
        if (url != null) {
            try {
                result.chartSourceLink = new URL(url);
            } catch (MalformedURLException e) {
                this.logger.warn("Invalid URL configured for chart [{}]: [{}]", prefix, url);
            }
        }
        return result;
    }

    /**
     * Validate that the configured settings are valid: limits are in order, labels are present, grid fits evenly.
     *
     * @param settings the settings read from the configuration file
     * @return {@code true} if the settings are valid, {@code false} otherwise; encountered problems are logged
     */
    private boolean validateChart(SimpleMeasurementsChartConfiguration settings) {
        boolean isValid = true;
        isValid = validateLimits(settings) && isValid;
        isValid = validateGrid(settings) && isValid;
        isValid = validateLabels(settings) && isValid;
        return isValid;
    }

    /**
     * Validate that the configured age and value limits are valid.
     *
     * @param settings the settings read from the configuration file
     * @return {@code true} if the settings are valid, {@code false} otherwise; encountered problems are logged
     */
    private boolean validateLimits(SimpleMeasurementsChartConfiguration settings) {
        boolean isValid = true;
        if (settings.lowerAgeLimit >= settings.upperAgeLimit) {
            this.logger.warn("Invalid chart settings for [{}]: age limits missing or out of order: [{}] and [{}]",
                    settings.chartIdentifier, settings.lowerAgeLimit, settings.upperAgeLimit);
            isValid = false;
        }
        if (settings.lowerValueLimit >= settings.upperValueLimit) {
            this.logger.warn("Invalid chart settings for [{}]: value limits missing or out of order: [{}] and [{}]",
                    settings.chartIdentifier, settings.lowerValueLimit, settings.upperValueLimit);
            isValid = false;
        }
        return isValid;
    }

    /**
     * Validate that the configured grid is valid.
     *
     * @param settings the settings read from the configuration file
     * @return {@code true} if the settings are valid, {@code false} otherwise; encountered problems are logged
     */
    private boolean validateGrid(SimpleMeasurementsChartConfiguration settings) {
        boolean isValid = true;
        if ((settings.upperAgeLimit - settings.lowerAgeLimit) % settings.ageTickStep != 0) {
            this.logger.warn("Invalid chart settings for [{}]: age grid lines don't fit evenly: [{}] in [{}-{}]",
                    settings.chartIdentifier, settings.ageTickStep, settings.lowerAgeLimit, settings.upperAgeLimit);
            isValid = false;
        }
        if (settings.ageLabelStep % settings.ageTickStep != 0) {
            this.logger.warn("Invalid chart settings for [{}]: major/minor age grid lines don't match: [{}]/[{}]",
                    settings.chartIdentifier, settings.ageLabelStep, settings.ageTickStep);
            isValid = false;
        }
        if (Math.abs(Math.IEEEremainder(settings.upperValueLimit - settings.lowerValueLimit,
                settings.valueTickStep)) > 1.0E-10) {
            this.logger.warn("Invalid chart settings for [{}]: value grid lines don't fit evenly: [{}] in [{}-{}]",
                    settings.chartIdentifier, settings.valueTickStep, settings.lowerValueLimit,
                    settings.upperValueLimit);
            isValid = false;
        }
        if (Math.abs(Math.IEEEremainder(settings.valueLabelStep, settings.valueTickStep)) > 1.0E-10) {
            this.logger.warn("Invalid chart settings for [{}]: major/minor value grid lines don't match: [{}]/[{}]",
                    settings.chartIdentifier, settings.valueLabelStep, settings.valueTickStep);
            isValid = false;
        }
        return isValid;
    }

    /**
     * Validate that all the labels are configured.
     *
     * @param settings the settings read from the configuration file
     * @return {@code true} if the settings are valid, {@code false} otherwise; encountered problems are logged
     */
    private boolean validateLabels(SimpleMeasurementsChartConfiguration settings) {
        boolean isValid = true;
        if (StringUtils.isBlank(settings.chartTitle)) {
            this.logger.warn("Invalid chart settings for [{}]: missing chart title", settings.chartIdentifier);
            isValid = false;
        }
        if (StringUtils.isBlank(settings.topLabel) && StringUtils.isBlank(settings.bottomLabel)) {
            this.logger.warn("Invalid chart settings for [{}]: missing top and bottom labels",
                    settings.chartIdentifier);
            isValid = false;
        }
        if (StringUtils.isBlank(settings.leftLabel) && StringUtils.isBlank(settings.rightLabel)) {
            this.logger.warn("Invalid chart settings for [{}]: missing left and right labels",
                    settings.chartIdentifier);
            isValid = false;
        }
        return isValid;
    }

    /**
     * Read and return a setting from the configuration, parsing it as an {@code int} number, falling back on the
     * provided default value.
     *
     * @param settingName the name of the setting to read
     * @param defaultValue the default value to use when there's no value specified in the configuration, or the
     *            specified value is not a valid number
     * @param configuration the configuration bundle with all the settings
     * @return the configured value, if one is configured as a valid {@code int} number, or the default value otherwise
     */
    private int getIntSetting(String settingName, int defaultValue, ResourceBundle configuration) {
        int result = defaultValue;
        if (configuration.containsKey(settingName)) {
            try {
                result = Integer.parseInt(configuration.getString(settingName));
                if (result < 0) {
                    this.logger.warn(
                            "Invalid chart settings for [{}]: value should be a positive integer, was [{}]",
                            settingName, configuration.getString(settingName));
                    result = defaultValue;
                }
            } catch (NumberFormatException ex) {
                // Fall back to the default value
                this.logger.warn("Invalid chart settings for [{}]: invalid integer [{}]", settingName,
                        configuration.getString(settingName));
            }
        }
        return result;
    }

    /**
     * Read and return a setting from the configuration, parsing it as a {@code double} number, falling back on the
     * provided default value.
     *
     * @param settingName the name of the setting to read
     * @param defaultValue the default value to use when there's no value specified in the configuration, or the
     *            specified value is not a valid double number
     * @param configuration the configuration bundle with all the settings
     * @return the configured value, if one is configured as a valid {@code double} number, or the default value
     *         otherwise
     */
    private double getDoubleSetting(String settingName, double defaultValue, ResourceBundle configuration) {
        double result = defaultValue;
        if (configuration.containsKey(settingName)) {
            try {
                result = Double.parseDouble(configuration.getString(settingName));
                if (Double.isNaN(result) || Double.isInfinite(result)) {
                    this.logger.warn("Invalid chart settings for [{}]: value should be finite, was [{}]",
                            settingName, configuration.getString(settingName));
                    result = defaultValue;
                }
            } catch (NumberFormatException ex) {
                // Fall back to the default value
                this.logger.warn("Invalid chart settings for [{}]: invalid double [{}]", settingName,
                        configuration.getString(settingName));
            }
        }
        return result;
    }

    /**
     * Read and return a setting from the configuration, parsing it as a positive {@code double} number, falling back on
     * the provided default value.
     *
     * @param settingName the name of the setting to read
     * @param defaultValue the default value to use when there's no value specified in the configuration, or the
     *            specified value is not a valid double number
     * @param configuration the configuration bundle with all the settings
     * @return the configured value, if one is configured as a valid {@code double} number, or the default value
     *         otherwise
     */
    private double getPositiveDoubleSetting(String settingName, double defaultValue, ResourceBundle configuration) {
        double result = getDoubleSetting(settingName, defaultValue, configuration);
        if (result < 0) {
            this.logger.warn("Invalid chart settings for [{}]: value should be positive, was [{}]", settingName,
                    configuration.getString(settingName));
            result = defaultValue;
        }
        return result;
    }

    /**
     * Read and return a setting from the configuration, falling back on the provided default value.
     *
     * @param settingName the name of the setting to read
     * @param defaultValue the default value to use when there's no value specified in the configuration; can be
     *            {@code null} or the empty string
     * @param configuration the configuration bundle with all the settings
     * @return the configured value, if one is configured (even as an empty string), or the default value otherwise
     */
    private String getStringSetting(String settingName, String defaultValue, ResourceBundle configuration) {
        String result = defaultValue;
        if (configuration.containsKey(settingName)) {
            result = configuration.getString(settingName);
        }
        return result;
    }
}