# ConfigurationContainer.java

    /*
     * Copyright (C) 2011 Everit Kft. (http://www.everit.biz)
     *
     * 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 org.everit.persistence.lqmg.internal;

    import java.io.File;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;

    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.JAXBElement;
    import javax.xml.bind.JAXBException;
    import javax.xml.bind.Unmarshaller;

    import org.everit.persistence.lqmg.LQMGException;
    import org.everit.persistence.lqmg.internal.schema.xml.AbstractNamingRuleType;
    import org.everit.persistence.lqmg.internal.schema.xml.ClassNameRuleType;
    import org.everit.persistence.lqmg.internal.schema.xml.LQMGType;
    import org.everit.persistence.lqmg.internal.schema.xml.NamingRulesType;
    import org.everit.persistence.lqmg.internal.schema.xml.RegexRuleType;
    import org.osgi.framework.Bundle;
    import org.osgi.framework.wiring.BundleWiring;

    import com.querydsl.sql.SchemaAndTable;

    /**
     * The configuration container used to code generation.
     */
    public class ConfigurationContainer {

      /**
       * {@link ConfigValue} with <code>null</code>.
       */
      private static class NullConfigValue extends ConfigValue<AbstractNamingRuleType> {

        NullConfigValue() {
          super(null, null, null);
        }

      }

      private final Map<SchemaAndTable, ConfigValue<? extends AbstractNamingRuleType>> cache =
          new HashMap<>();

      private final Map<SchemaAndTable, ConfigValue<ClassNameRuleType>> classNameRuleMap =
          new HashMap<>();

      private final JAXBContext jaxbContext;

      private final Map<SchemaAndTable, ConfigValue<ClassNameRuleType>> mainClassNameRuleMap =
          new HashMap<>();

      private final Map<SchemaAndTable, ConfigValue<RegexRuleType>> mainRegexRuleMap =
          new HashMap<>();

      private final Map<String, Pattern> patternsByRegex = new HashMap<>();

      private final Set<ConfigPath> processedConfigs = new HashSet<>();

      private final Map<SchemaAndTable, ConfigValue<RegexRuleType>> regexRuleMap =
          new HashMap<>();

      /**
       * Constructor.
       */
      public ConfigurationContainer() {
        try {
          jaxbContext = JAXBContext.newInstance(LQMGType.class.getPackage().getName(), this.getClass()
              .getClassLoader());
        } catch (JAXBException e) {
          throw new LQMGException("Could not create JAXBContext for configuration", e);
        }
      }

      /**
       * Adds a configuration.
       */
      public void addConfiguration(final ConfigPath configPath) {
        if (!processedConfigs.add(configPath)) {
          // If the config file is already processed, just return
          return;
        }
        Bundle bundle = configPath.bundle;
        String resource = configPath.resource;
        URL configurationURL;
        if (bundle == null) {
          try {
            configurationURL = new File(configPath.resource).toURI().toURL();
          } catch (MalformedURLException e) {
            throw new LQMGException(
                "Could not read configuration from path " + configPath.resource, e);
          }
        } else {
          BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
          ClassLoader classLoader = bundleWiring.getClassLoader();
          configurationURL = classLoader.getResource(resource);
        }

        if (configurationURL == null) {
          throw new LQMGException(
              "Configuration file not found on the specified path '" + resource + "' in bundle "
                  + bundle.toString(),
              null);
        }
        LQMGType lqmgType = unmarshalConfiguration(configurationURL);
        processLQMGType(lqmgType, resource, bundle);
      }

      private <T extends AbstractNamingRuleType> void addValueToConfigMap(
          final SchemaAndTable schemaAndTable,
          final ConfigValue<T> configValue,
          final Map<SchemaAndTable, ConfigValue<T>> configMap) {

        ConfigValue<? extends AbstractNamingRuleType> cachedValue = cache.get(schemaAndTable);
        if (cachedValue != null) {
          cache.remove(schemaAndTable);
        }

        ConfigValue<T> existingValue = configMap.get(schemaAndTable);
        if (existingValue != null) {
          StringBuilder sb = new StringBuilder("Configuration is defined more than once: ").append(
              schemaAndTable.toString()).append("\n");

          Bundle bundle = configValue.bundle;
          if (bundle != null) {
            sb.append("  Bundle: ").append(bundle.toString()).append("; ");
          }
          sb.append("Path: ").append(configValue.configurationXMLPath).append("\n");

          Bundle existingValueBundle = existingValue.bundle;
          if (existingValueBundle != null) {
            sb.append("  Bundle: ").append(existingValueBundle.toString()).append("; ");
          }
          sb.append("Path: ").append(existingValue.configurationXMLPath);

          throw new LQMGException(sb.toString(), null);
        }
        configMap.put(schemaAndTable, configValue);
      }

      /**
       * Finds a {@link ConfigValue} based on schemaName and entityName.
       */
      public ConfigValue<? extends AbstractNamingRuleType> findConfigForEntity(
          final SchemaAndTable schemaAndTable) {

        ConfigValue<? extends AbstractNamingRuleType> configValue = cache.get(schemaAndTable);

        if (configValue != null) {
          if (configValue instanceof NullConfigValue) {
            return null;
          } else {
            return configValue;
          }
        }

        configValue = findEntityConfigInMap(schemaAndTable, mainClassNameRuleMap);
        if (configValue != null) {
          return configValue;
        }
        configValue = findRegexRuleInMap(schemaAndTable, mainRegexRuleMap);
        if (configValue != null) {
          return configValue;
        }
        configValue = findEntityConfigInMap(schemaAndTable, classNameRuleMap);
        if (configValue != null) {
          return configValue;
        }

        return findRegexRuleInMap(schemaAndTable, regexRuleMap);
      }

      private ConfigValue<ClassNameRuleType> findEntityConfigInMap(final SchemaAndTable schemaAndTable,
          final Map<SchemaAndTable, ConfigValue<ClassNameRuleType>> map) {

        ConfigValue<ClassNameRuleType> configValue = map.get(schemaAndTable);

        // No schema matched record, trying to search on records where schema is not defined
        if (configValue == null) {
          SchemaAndTable noSchemaAndTable = new SchemaAndTable(null, schemaAndTable.getTable());
          configValue = map.get(noSchemaAndTable);
        }
        return configValue;
      }

      private ConfigValue<RegexRuleType> findRegexRuleInMap(final SchemaAndTable schemaAndTable,
          final Map<SchemaAndTable, ConfigValue<RegexRuleType>> map) {

        List<ConfigValue<RegexRuleType>> result = new ArrayList<ConfigValue<RegexRuleType>>();

        String schemaName = schemaAndTable.getSchema();
        String tableName = schemaAndTable.getTable();

        if (schemaName != null) {
          for (Entry<SchemaAndTable, ConfigValue<RegexRuleType>> entry : map.entrySet()) {
            SchemaAndTable entryKey = entry.getKey();
            ConfigValue<RegexRuleType> configValue = entry.getValue();
            String regex = configValue.namingRule.getRegex();
            if (schemaName.equals(entryKey.getSchema()) && matches(regex, tableName)) {
              result.add(configValue);
            }
          }
        }
        validateConfigValueResultSize(schemaAndTable, result);
        if (result.size() == 1) {
          return result.get(0);
        }

        // No schema matched record, trying to search on records where schema is not defined
        for (Entry<SchemaAndTable, ConfigValue<RegexRuleType>> entry : map.entrySet()) {
          ConfigValue<RegexRuleType> configValue = entry.getValue();
          RegexRuleType regexRule = configValue.namingRule;
          if ((regexRule.getSchema() == null) && matches(regexRule.getRegex(), tableName)) {
            result.add(configValue);
          }
        }

        validateConfigValueResultSize(schemaAndTable, result);
        if (result.size() == 1) {
          return result.get(0);
        }
        return null;
      }

      /**
       * Returns the cached and compiled {@link Pattern} by the given regex.
       */
      public Pattern getPatternByRegex(final String regex) {
        Pattern pattern = patternsByRegex.get(regex);
        if (pattern == null) {
          pattern = Pattern.compile(regex);
          patternsByRegex.put(regex, pattern);
        }
        return pattern;
      }

      private boolean matches(final String regex, final String value) {
        Pattern pattern = getPatternByRegex(regex);
        Matcher matcher = pattern.matcher(value);

        return matcher.matches();
      }

      private void processClassNameRuleType(final String xmlConfigurationPath, final Bundle bundle,
          final AbstractNamingRuleType lqmgAbstractEntity) {
        ClassNameRuleType lqmgEntity = (ClassNameRuleType) lqmgAbstractEntity;
        SchemaAndTable schemaAndTable = new SchemaAndTable(
            lqmgEntity.getSchema(), lqmgEntity.getEntity());
        ConfigValue<ClassNameRuleType> configValue =
            new ConfigValue<ClassNameRuleType>(lqmgEntity,
                bundle, xmlConfigurationPath);

        validatePackage(configValue);
        if (bundle == null) {
          addValueToConfigMap(schemaAndTable, configValue, mainClassNameRuleMap);
        } else {
          addValueToConfigMap(schemaAndTable, configValue, classNameRuleMap);
        }
      }

      private void processLQMGType(final LQMGType lqmgType, final String xmlConfigurationPath,
          final Bundle bundle) {
        String defaultPackageName = lqmgType.getDefaultPackage();
        String defaultSchemaName = lqmgType.getDefaultSchema();

        NamingRulesType entities = lqmgType.getNamingRules();
        if (entities == null) {
          return;
        }

        List<AbstractNamingRuleType> entityAndEntitySet = entities.getClassNameRuleAndRegexRule();
        for (AbstractNamingRuleType lqmgAbstractEntity : entityAndEntitySet) {
          if (lqmgAbstractEntity.getPackage() == null) {
            lqmgAbstractEntity.setPackage(defaultPackageName);
          }

          if (lqmgAbstractEntity.getSchema() == null) {
            lqmgAbstractEntity.setSchema(defaultSchemaName);
          }

          if (lqmgAbstractEntity.getPrefix() == null) {
            lqmgAbstractEntity.setPrefix(lqmgType.getDefaultPrefix());
          }

          if (lqmgAbstractEntity.getSuffix() == null) {
            lqmgAbstractEntity.setSuffix(lqmgType.getDefaultSuffix());
          }

          if (lqmgAbstractEntity instanceof ClassNameRuleType) {
            processClassNameRuleType(xmlConfigurationPath, bundle, lqmgAbstractEntity);
          } else if (lqmgAbstractEntity instanceof RegexRuleType) {
            processRegexRuleType(xmlConfigurationPath, bundle, lqmgAbstractEntity);
          } else {
            throw new IllegalArgumentException(
                "Unsupported naming rule type [" + lqmgAbstractEntity + "]");
          }
        }
      }

      private void processRegexRuleType(final String xmlConfigurationPath, final Bundle bundle,
          final AbstractNamingRuleType lqmgAbstractEntity) {
        RegexRuleType lqmgEntitySet = (RegexRuleType) lqmgAbstractEntity;
        SchemaAndTable configKey = new SchemaAndTable(
            lqmgEntitySet.getSchema(), lqmgEntitySet.getRegex());
        ConfigValue<RegexRuleType> configValue = new ConfigValue<RegexRuleType>(lqmgEntitySet,
            bundle, xmlConfigurationPath);

        validatePackage(configValue);
        if (bundle == null) {
          addValueToConfigMap(configKey, configValue, mainRegexRuleMap);
        } else {
          addValueToConfigMap(configKey, configValue, regexRuleMap);
        }
      }

      private void throwMultipleMatchingRegexException(final SchemaAndTable schemaAndTable,
          final List<ConfigValue<RegexRuleType>> matchingConfigs) {

        StringBuilder sb = new StringBuilder("Cannot decide which configuration should be applied to '")
            .append(schemaAndTable.getTable()).append("' table of '").append(schemaAndTable.getSchema())
            .append("' schema. Found matchings:\n");

        for (ConfigValue<RegexRuleType> configValue : matchingConfigs) {
          RegexRuleType namingRule = configValue.namingRule;
          sb.append("  Bundle: ").append(configValue.bundle).append("; XMLPath: ")
              .append(configValue.configurationXMLPath).append("; Schema: ")
              .append(namingRule.getSchema())
              .append("; Regex: ").append(namingRule.getRegex());
        }
        throw new LQMGException(sb.toString(), null);
      }

      private LQMGType unmarshalConfiguration(final URL configurationURL) {
        Unmarshaller unmarshaller;
        try {
          unmarshaller = jaxbContext.createUnmarshaller();

          @SuppressWarnings("unchecked")
          JAXBElement<LQMGType> rootElement =
              (JAXBElement<LQMGType>) unmarshaller.unmarshal(configurationURL);
          return rootElement.getValue();
        } catch (JAXBException e) {
          throw new LQMGException(
              "Could not unmarshal LQMG configuration: " + configurationURL.toExternalForm(), e);
        }
      }

      private void validateConfigValueResultSize(final SchemaAndTable schemaAndTable,
          final List<ConfigValue<RegexRuleType>> result) {
        if (result.size() > 1) {
          throwMultipleMatchingRegexException(schemaAndTable, result);
        }
      }

      private void validatePackage(final ConfigValue<? extends AbstractNamingRuleType> configValue) {
        AbstractNamingRuleType namingRule = configValue.namingRule;
        if ((namingRule.getPackage() != null) && !"".equals(namingRule.getPackage().trim())) {
          return;
        }
        StringBuilder sb = new StringBuilder("Missing java package: ConfigValue [bundle=")
            .append(configValue.bundle).append(", configurationXMLPath=")
            .append(configValue.configurationXMLPath).append(", namingRule.schema=")
            .append(namingRule.getSchema()).append(", ");

        if (namingRule instanceof RegexRuleType) {
          sb.append("namingRule.regex=").append(((RegexRuleType) namingRule).getRegex());
        } else if (namingRule instanceof ClassNameRuleType) {
          sb.append("namingRule.class=").append(((ClassNameRuleType) namingRule).getClazz());
        }
        sb.append("].");
        throw new LQMGException(sb.toString(), null);
      }
    }