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

de.codecamp.messages.shared.conf.ProjectConf Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
package de.codecamp.messages.shared.conf;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import de.codecamp.messages.shared.bundle.MessageBundleManager;
import de.codecamp.messages.shared.bundle.NioFileSystemAdapter;
import de.codecamp.messages.shared.messageformat.DefaultMessageFormatSupport;
import de.codecamp.messages.shared.messageformat.IcuMessageFormatSupport;
import de.codecamp.messages.shared.model.MessageModule;


/**
 * {@link ProjectConf} contains the aspects of the project configuration shared by the annotation
 * processor and other tools like the Eclipse companion plug-in. It represents a snapshot of the
 * current configuration, so it's not intended to be long lived or stored for later use.
 */
public class ProjectConf
  implements
    MessageModule
{

  public static final String ERROR_CONF_FILE_ERROR = "ProjectConfFileError";

  public static final String ERROR_MISSING_OPTION = "MissingOption";

  public static final String ERROR_ILLEGAL_LOCALE = "IllegalLocale";

  public static final String ERROR_ILLEGAL_BUNDLE_MAPPING = "IllegalBundleMapping";

  public static final String ERROR_ILLEGAL_TYPE_ABBREVIATION = "IllegalTypeAbbreviation";

  public static final String ERROR_ILLEGAL_IMPORT = "IllegalImport";

  public static final String ERROR_MISSING_IMPORT = "MissingImport";

  public static final String ERROR_UNKNOWN_VALUE = "UnknownValue";


  public static final String PROJECT_CONF_FILE_NAME = "messages-conf.properties";


  public static final String CONF_PREFIX = "messages.";

  public static final String CONF_PROJECT_DIR = CONF_PREFIX + "projectDir";

  public static final String CONF_MODULE_NAME = CONF_PREFIX + "moduleName";

  public static final String CONF_BUNDLE_DIR = CONF_PREFIX + "bundleDir";

  public static final String BUNDLE_DIR_DEFAULT = "src/main/resources/messages";

  public static final String CONF_BUNDLE_ENCODING = CONF_PREFIX + "bundleEncoding";

  public static final String CONF_BUNDLES = CONF_PREFIX + "bundles";

  private static final String BUNDLES_DEFAULT = ":messages";

  private static final String BUNDLES_IMPORTS = "$imports";

  public static final String CONF_IGNORED_BUNDLES = CONF_PREFIX + "ignoredBundles";

  public static final String CONF_TARGET_LOCALES = CONF_PREFIX + "targetLocales";

  public static final String CONF_IMPORTS = CONF_PREFIX + "imports";

  public static final String CONF_TYPE_ABBREVIATIONS = CONF_PREFIX + "typeAbbreviations";

  public static final String CONF_MODE = CONF_PREFIX + "mode";

  public static final String CONF_MISSING_MESSAGE_POLICY = CONF_PREFIX + "missingMessagePolicy";

  public static final String CONF_MESSAGE_ARG_POLICY = CONF_PREFIX + "messageArgPolicy";

  public static final String CONF_UNDECLARED_KEY_POLICY = CONF_PREFIX + "undeclaredKeyPolicy";

  public static final String CONF_UNDECLARED_KEY_COMMENT = CONF_PREFIX + "undeclaredKeyComment";

  private static final String UNDECLARED_KEY_COMMENT_DEFAULT = "FIXME undeclared message key";

  public static final String CONF_BUNDLE_MISMATCH_POLICY = CONF_PREFIX + "bundleMismatchPolicy";


  public static final String CONF_MESSAGE_FORMAT = CONF_PREFIX + "messageFormat";


  public static final String CONF_GENERATE_CONSTANTS = CONF_PREFIX + "generateConstants";

  public static final String CONF_GENERATE_PROXIES = CONF_PREFIX + "generateProxies";


  public static final List ALL_CONF_NAMES;
  static
  {
    List confNames = Arrays.asList(CONF_MODULE_NAME, CONF_PROJECT_DIR, CONF_BUNDLE_DIR,
        CONF_BUNDLE_ENCODING, CONF_BUNDLES, CONF_IGNORED_BUNDLES, CONF_TARGET_LOCALES, CONF_IMPORTS,
        CONF_MODE, CONF_MISSING_MESSAGE_POLICY, CONF_MESSAGE_ARG_POLICY, CONF_UNDECLARED_KEY_POLICY,
        CONF_UNDECLARED_KEY_COMMENT, CONF_BUNDLE_MISMATCH_POLICY, CONF_MESSAGE_FORMAT,
        CONF_GENERATE_CONSTANTS, CONF_GENERATE_PROXIES);
    ALL_CONF_NAMES = Collections.unmodifiableList(confNames);
  }


  private final String moduleName;

  private final String projectDir;

  private final String bundleDir;

  private final Charset bundleEncoding;

  private final Set ignoredBundles;

  private final List bundleMappings;

  private final List targetLocales;

  private final List imports;

  private final Map typeAbbreviations;


  private final Mode mode;

  private final MissingMessagePolicy missingMessagePolicy;

  private final MessageArgPolicy messageArgPolicy;

  private final UndeclaredKeyPolicy undeclaredKeyPolicy;

  private final String undeclaredKeyComment;

  private final BundleMismatchPolicy bundleMismatchPolicy;


  private final String messageFormat;


  private final boolean generateConstants;

  private final boolean generateProxies;


  private List resolvedBundleMappings;


  public ProjectConf(ConfValueProvider confProvider)
    throws ProjectConfException
  {
    this.projectDir =
        parseString(confProvider.getConf(CONF_PROJECT_DIR), CONF_PROJECT_DIR, true, null);

    ConfValueProvider extConfProvider = null;
    if (projectDir != null)
    {
      Path messagesPropsPath = Paths.get(projectDir, PROJECT_CONF_FILE_NAME);
      if (Files.isRegularFile(messagesPropsPath))
      {
        try (InputStream in = Files.newInputStream(messagesPropsPath))
        {
          Properties messagesProperties = new Properties();
          messagesProperties.load(in);
          extConfProvider = (name) -> {
            String value = confProvider.getConf(name);
            if (value == null)
              value = messagesProperties.getProperty(name);
            if (value == null)
              value = messagesProperties.getProperty(StringUtils.removeStart(name, CONF_PREFIX));
            return value;
          };
        }
        catch (IOException ex)
        {
          String msg = "Failed to read %s in project directory '%s'.";
          msg = String.format(msg, PROJECT_CONF_FILE_NAME, projectDir);
          throw new ProjectConfException(msg, ex, ERROR_CONF_FILE_ERROR, projectDir);
        }
      }
    }

    if (extConfProvider == null)
      extConfProvider = confProvider;

    this.moduleName = parseString(extConfProvider.getConf(CONF_MODULE_NAME), CONF_MODULE_NAME, true,
        "application");
    this.bundleEncoding = parseBundleEncoding(extConfProvider.getConf(CONF_BUNDLE_ENCODING));
    this.ignoredBundles = parseIgnoredBundles(extConfProvider.getConf(CONF_IGNORED_BUNDLES));
    this.bundleMappings = parseBundles(extConfProvider.getConf(CONF_BUNDLES), getModuleName());
    this.targetLocales = parseLocales(extConfProvider.getConf(CONF_TARGET_LOCALES));
    this.imports = parseImports(extConfProvider.getConf(CONF_IMPORTS));
    this.typeAbbreviations =
        parseTypeAbbreviations(extConfProvider.getConf(CONF_TYPE_ABBREVIATIONS));

    this.mode = parseEnum(extConfProvider.getConf(CONF_MODE), Mode.class, Mode.RELEASE);

    this.missingMessagePolicy = parseEnum(extConfProvider.getConf(CONF_MISSING_MESSAGE_POLICY),
        MissingMessagePolicy.class, mode.missingMessagePolicy());

    this.messageArgPolicy = parseEnum(extConfProvider.getConf(CONF_MESSAGE_ARG_POLICY),
        MessageArgPolicy.class, mode.messageArgPolicy());

    this.undeclaredKeyPolicy = parseEnum(extConfProvider.getConf(CONF_UNDECLARED_KEY_POLICY),
        UndeclaredKeyPolicy.class, mode.undeclaredKeyPolicy());

    this.undeclaredKeyComment = parseString(extConfProvider.getConf(CONF_UNDECLARED_KEY_COMMENT),
        CONF_UNDECLARED_KEY_COMMENT, true, UNDECLARED_KEY_COMMENT_DEFAULT);

    this.bundleMismatchPolicy = parseEnum(extConfProvider.getConf(CONF_BUNDLE_MISMATCH_POLICY),
        BundleMismatchPolicy.class, mode.bundleMismatchPolicy());

    this.bundleDir = parseString(extConfProvider.getConf(CONF_BUNDLE_DIR), CONF_BUNDLE_DIR, true,
        BUNDLE_DIR_DEFAULT);

    this.messageFormat = parseString(extConfProvider.getConf(CONF_MESSAGE_FORMAT), CONF_BUNDLE_DIR,
        true, DefaultMessageFormatSupport.ID);
    if (messageFormat != null && !messageFormat.equals(DefaultMessageFormatSupport.ID)
        && !messageFormat.equals(IcuMessageFormatSupport.ID))
    {
      String msg = "Unknown value '%s' for %s.";
      msg = String.format(msg, messageFormat, CONF_MESSAGE_FORMAT);
      throw new ProjectConfException(msg, ERROR_UNKNOWN_VALUE, messageFormat, CONF_MESSAGE_FORMAT);
    }

    this.generateConstants = parseBoolean(extConfProvider.getConf(CONF_GENERATE_CONSTANTS), true);
    this.generateProxies = parseBoolean(extConfProvider.getConf(CONF_GENERATE_PROXIES), true);
  }


  public String getProjectDir()
  {
    return projectDir;
  }

  @Override
  public String getModuleName()
  {
    return moduleName;
  }

  public String getBundleDir()
  {
    if (projectDir != null)
    {
      return Paths.get(projectDir, bundleDir).toString();
    }
    else
    {
      return bundleDir;
    }
  }

  public Charset getBundleEncoding()
  {
    return bundleEncoding;
  }

  public Set getIgnoredBundles()
  {
    return ignoredBundles;
  }

  @Override
  public List getBundleMappings()
  {
    return bundleMappings;
  }

  @Override
  public List getTargetLocales()
  {
    return targetLocales;
  }

  @Override
  public List getImportedModules()
  {
    return getImports();
  }

  public List getImports()
  {
    return imports;
  }

  public Map getTypeAbbreviations()
  {
    return typeAbbreviations;
  }

  public Mode getMode()
  {
    return mode;
  }

  public MissingMessagePolicy getMissingMessagePolicy()
  {
    return missingMessagePolicy;
  }

  public MessageArgPolicy getMessageArgPolicy()
  {
    return messageArgPolicy;
  }

  public UndeclaredKeyPolicy getUndeclaredKeyPolicy()
  {
    return undeclaredKeyPolicy;
  }

  public String getUndeclaredKeyComment()
  {
    return undeclaredKeyComment;
  }

  public BundleMismatchPolicy getBundleMismatchPolicy()
  {
    return bundleMismatchPolicy;
  }

  @Override
  public String getMessageFormat()
  {
    return messageFormat;
  }

  public boolean getGenerateConstants()
  {
    return generateConstants;
  }

  public boolean getGenerateProxies()
  {
    return generateProxies;
  }


  // private static String parseString(String rawValue, String optionKey, boolean blankToNull)
  // throws ProjectConfException
  // {
  // String value = parseString(rawValue, optionKey, blankToNull, null);
  // if (value == null)
  // {
  // String msg = "Option '%s' must be set.";
  // msg = String.format(msg, optionKey);
  // throw new ProjectConfException(msg, ERROR_MISSING_OPTION, optionKey);
  // }
  //
  // return value;
  // }

  private static String parseString(String rawValue, String optionKey, boolean blankToNull,
      String defaultValue)
    throws ProjectConfException
  {
    String value = rawValue;

    if (blankToNull)
      value = StringUtils.defaultIfBlank(value, null);

    value = StringUtils.trim(value);

    if (value == null)
    {
      value = defaultValue;
    }

    return value;
  }

  private static Charset parseBundleEncoding(String rawValue)
    throws ProjectConfException
  {
    if (StringUtils.isBlank(rawValue))
      return StandardCharsets.UTF_8;

    rawValue = StringUtils.trim(rawValue);

    try
    {
      return Charset.forName(rawValue);
    }
    catch (IllegalCharsetNameException | UnsupportedCharsetException ex)
    {
      throw (ProjectConfException) new ProjectConfException(
          "Message bundle encoding '%s' not supported.", ERROR_UNKNOWN_VALUE, rawValue)
              .initCause(ex);
    }
  }

  /**
   * @param rawValue
   *          the raw value
   * @param moduleName
   *          the module name
   * @return the message bundle mappings, sorted from longest to shortest package
   * @throws IllegalArgumentException
   */
  private static List parseBundles(String rawValue, String moduleName)
    throws ProjectConfException
  {
    if (StringUtils.isBlank(rawValue))
      rawValue = BUNDLES_DEFAULT;

    List bundleMappings = new ArrayList<>();
    for (String bundleToken : StringUtils.split(rawValue, ","))
    {
      bundleToken = bundleToken.trim();

      if (bundleToken.equals(BUNDLES_IMPORTS))
      {
        bundleMappings.add(BundleMapping.IMPORTS_PLACEHOLDER);
        continue;
      }

      if (!bundleToken.contains(":"))
      {
        String msg = "Illegal message bundle mapping: %s";
        msg = String.format(msg, bundleToken);
        throw new ProjectConfException(msg, ERROR_ILLEGAL_BUNDLE_MAPPING, bundleToken);
      }

      String[] tokens = StringUtils.splitPreserveAllTokens(bundleToken, ":", 2);

      String packageName = tokens[0].trim();
      String bundleName = tokens[1].trim();

      if (StringUtils.isNotBlank(moduleName))
      {
        /*
         * According to Javadoc of ResourceBundle, base names (=bundle names) should always be fully
         * qualified class names. I.e. they should be using '.' instead of '/', even though '/' will
         * work for Properties-based resource bundles.
         */
        bundleName = (moduleName + "." + bundleName).replace('/', '.');
      }

      if (bundleToken.contains("_"))
      {
        String msg =
            "Underscore (_) now allowed within message bundle name. It's reserved for the locale: %s";
        msg = String.format(msg, bundleToken);
        throw new ProjectConfException(msg, ERROR_ILLEGAL_BUNDLE_MAPPING, bundleName);
      }

      bundleMappings.add(new BundleMapping(packageName, bundleName));
    }

    return ImmutableList.copyOf(bundleMappings);
  }

  private static Set parseIgnoredBundles(String rawValue)
  {
    if (StringUtils.isBlank(rawValue))
      return Collections.emptySet();

    Set ignoredBundles = Stream.of(StringUtils.split(rawValue, ","))
        .filter(StringUtils::isNotBlank).map(StringUtils::trim).collect(toSet());
    return ImmutableSet.copyOf(ignoredBundles);
  }

  /**
   * @param rawValue
   *          the raw value
   * @return the list of required locales
   * @throws IllegalArgumentException
   */
  private static List parseLocales(String rawValue, Locale... defaults)
    throws ProjectConfException
  {
    List locales = new ArrayList<>();

    if (!StringUtils.isBlank(rawValue))
    {
      List localeStrings = Stream.of(StringUtils.split(rawValue, ","))
          .filter(StringUtils::isNotBlank).map(StringUtils::trim).collect(toList());
      for (String localeString : localeStrings)
      {
        Locale locale = Locale.forLanguageTag(localeString);
        if (locale.getLanguage().isEmpty() && localeString.contains("_"))
          locale = LocaleUtils.toLocale(localeString);

        if (locale.getLanguage().isEmpty())
        {
          String msg = "The locale string '%s' did not contain a language.";
          msg = String.format(msg, localeString);
          throw new ProjectConfException(msg, ERROR_ILLEGAL_LOCALE, localeString);
        }

        locales.add(locale);
      }
    }

    if (locales.isEmpty())
      locales.addAll(Arrays.asList(defaults));

    if (locales.isEmpty())
    {
      String msg = "Option '%s' must be set.";
      msg = String.format(msg, CONF_TARGET_LOCALES);
      throw new ProjectConfException(msg, ERROR_MISSING_OPTION, CONF_TARGET_LOCALES);
    }

    return ImmutableList.copyOf(locales);
  }

  private List parseImports(String rawValue)
    throws ProjectConfException
  {
    if (StringUtils.isBlank(rawValue))
      return Collections.emptyList();

    List imports = Stream.of(StringUtils.split(rawValue, ","))
        .filter(StringUtils::isNotBlank).map(StringUtils::trim).collect(toList());
    return ImmutableList.copyOf(imports);
  }

  private static Map parseTypeAbbreviations(String rawValue)
    throws ProjectConfException
  {
    if (StringUtils.isBlank(rawValue))
      return Collections.emptyMap();

    Map typeAbbreviations = new HashMap<>();
    for (String abbreviationItem : StringUtils.split(rawValue, ","))
    {
      abbreviationItem = abbreviationItem.trim();

      if (!abbreviationItem.contains(":"))
      {
        String msg = "Illegal type abbreviation: %s";
        msg = String.format(msg, abbreviationItem);
        throw new ProjectConfException(msg, ERROR_ILLEGAL_TYPE_ABBREVIATION, abbreviationItem);
      }

      String[] tokens = StringUtils.splitPreserveAllTokens(abbreviationItem, ":", 2);

      String abbreviation = tokens[0].trim();
      String fullyQualifiedType = tokens[1].trim();

      typeAbbreviations.put(abbreviation, fullyQualifiedType);
    }

    return ImmutableMap.copyOf(typeAbbreviations);
  }


  private > T parseEnum(String rawValue, Class enumType)
    throws ProjectConfException
  {
    if (StringUtils.isEmpty(rawValue))
    {
      return null;
    }
    else
    {
      try
      {
        return Enum.valueOf(enumType, rawValue);
      }
      catch (IllegalArgumentException ex)
      {
        String msg = "Unknown value '%s' for %s.";
        msg = String.format(msg, rawValue, enumType.getSimpleName());
        throw new ProjectConfException(msg, ERROR_UNKNOWN_VALUE, rawValue,
            enumType.getSimpleName());
      }
    }
  }

  private > T parseEnum(String rawValue, Class enumType, T defaultValue)
    throws ProjectConfException
  {
    T e = parseEnum(rawValue, enumType);
    if (e == null)
      e = defaultValue;
    return e;
  }

  private Boolean parseBoolean(String rawValue, Boolean defaultValue)
  {
    Boolean result = BooleanUtils.toBooleanObject(rawValue, "true", "false", null);
    if (result == null)
      result = defaultValue;
    return result;
  }


  public void resolveBundleMappings(Function importedModuleProvider)
    throws ProjectConfException
  {
    resolvedBundleMappings = getImportedBundleMappings(this, importedModuleProvider);
  }

  private static List getImportedBundleMappings(MessageModule module,
      Function importedModuleProvider)
    throws ProjectConfException
  {
    List bundleMappings = new ArrayList<>(module.getBundleMappings());

    List importedBundleMappings = new ArrayList<>();
    if (module.getImportedModules() != null)
    {
      for (String importedModuleName : module.getImportedModules())
      {
        MessageModule importedModule = importedModuleProvider.apply(importedModuleName);
        if (importedModule == null)
        {
          String msg = "Imported messages module '%s' not found.";
          msg = String.format(msg, importedModuleName);
          throw new ProjectConfException(msg, ERROR_MISSING_IMPORT, importedModuleName);
        }

        importedBundleMappings
            .addAll(getImportedBundleMappings(importedModule, importedModuleProvider));
      }
    }

    int placeholderIndex = bundleMappings.indexOf(BundleMapping.IMPORTS_PLACEHOLDER);
    if (placeholderIndex > -1)
    {
      bundleMappings.remove(placeholderIndex);
      bundleMappings.addAll(placeholderIndex, importedBundleMappings);
    }
    else
    {
      bundleMappings.addAll(importedBundleMappings);
    }

    return bundleMappings;
  }

  /**
   * Returns the most specific bundle name for the given message key after applying the bundle root
   * mappings.
   *
   * @param messageKey
   *          a message key
   * @return the target bundle name for the given message key or null if no mapping is available
   */
  public Optional toTargetBundleName(String messageKey)
  {
    return toTargetBundleName(messageKey, false);
  }

  public Optional toTargetBundleName(String messageKey, boolean onlyLocalMappings)
  {
    if (!onlyLocalMappings && resolvedBundleMappings == null)
      throw new IllegalStateException("Message bundle mappings have not been resolved.");

    List mappings;
    if (onlyLocalMappings)
      mappings = bundleMappings;
    else
      mappings = resolvedBundleMappings;

    for (BundleMapping mapping : mappings)
    {
      Pattern messageKeyPattern = mapping.getMessageKeyPatternAsRegex();

      Matcher matcher = messageKeyPattern.matcher(messageKey);
      if (matcher.matches())
      {
        String bundleName = mapping.getBundleNamePattern();
        if (bundleName.contains("*") && matcher.groupCount() >= 1)
        {
          String subPackage = StringUtils.removeStart(matcher.group(1), ".");
          subPackage = StringUtils.substringBefore(subPackage, ".");

          bundleName = mapping.getBundleNamePattern().replace("*", subPackage);
        }

        return Optional.of(bundleName);
      }
    }
    return Optional.empty();
  }


  public MessageBundleManager createDefaultMessageBundleManager()
  {
    return new MessageBundleManager<>(this, new NioFileSystemAdapter());
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy