001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.model;
007
008import org.apache.logging.log4j.LogManager;
009import org.apache.logging.log4j.Logger;
010
011import java.util.Collection;
012import java.util.List;
013import java.util.Locale;
014import java.util.Map;
015import java.util.function.Function;
016import java.util.function.Predicate;
017import java.util.stream.Collectors;
018import java.util.stream.Stream;
019
020import dev.metaschema.core.qname.IEnhancedQName;
021import dev.metaschema.core.util.CollectionUtil;
022import dev.metaschema.core.util.CustomCollectors;
023import dev.metaschema.core.util.ObjectUtils;
024import edu.umd.cs.findbugs.annotations.NonNull;
025import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
026import nl.talsmasoftware.lazy4j.Lazy;
027
028/**
029 * Provides a common, abstract implementation of a {@link IModule}.
030 *
031 * @param <M>
032 *          the imported module Java type
033 * @param <D>
034 *          the definition Java type
035 * @param <FL>
036 *          the flag definition Java type
037 * @param <FI>
038 *          the field definition Java type
039 * @param <A>
040 *          the assembly definition Java type
041 */
042@SuppressWarnings("PMD.CouplingBetweenObjects")
043public abstract class AbstractModule<
044    M extends IModuleExtended<M, D, FL, FI, A>,
045    D extends IModelDefinition,
046    FL extends IFlagDefinition,
047    FI extends IFieldDefinition,
048    A extends IAssemblyDefinition>
049    implements IModuleExtended<M, D, FL, FI, A> {
050  private static final Logger LOGGER = LogManager.getLogger(AbstractModule.class);
051
052  @NonNull
053  private final List<? extends M> importedModules;
054  @NonNull
055  private final Lazy<Exports> exports;
056  @NonNull
057  private final Lazy<IEnhancedQName> qname;
058
059  /**
060   * Construct a new Metaschema module object.
061   *
062   * @param importedModules
063   *          the collection of Metaschema module objects this Metaschema module
064   *          imports
065   */
066  public AbstractModule(@NonNull List<? extends M> importedModules) {
067    this.importedModules
068        = CollectionUtil.unmodifiableList(ObjectUtils.requireNonNull(importedModules, "importedModules"));
069    this.exports = ObjectUtils.notNull(Lazy.of(() -> new Exports(importedModules)));
070    this.qname = ObjectUtils.notNull(Lazy.of(() -> IEnhancedQName.of(getXmlNamespace(), getShortName())));
071  }
072
073  @Override
074  public IEnhancedQName getQName() {
075    return ObjectUtils.notNull(qname.get());
076  }
077
078  @Override
079  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "interface doesn't allow modification")
080  public List<? extends M> getImportedModules() {
081    return importedModules;
082  }
083
084  @SuppressWarnings("null")
085  @NonNull
086  private Exports getExports() {
087    return exports.get();
088  }
089
090  private Map<String, ? extends M> getImportedModulesByShortName() {
091    return importedModules.stream().collect(Collectors.toMap(IModule::getShortName, Function.identity()));
092  }
093
094  @Override
095  public M getImportedModuleByShortName(String name) {
096    return getImportedModulesByShortName().get(name);
097  }
098
099  @SuppressWarnings("null")
100  @Override
101  public Collection<FL> getExportedFlagDefinitions() {
102    return getExports().getExportedFlagDefinitionMap().values();
103  }
104
105  @Override
106  public FL getExportedFlagDefinitionByName(IEnhancedQName name) {
107    return getExports().getExportedFlagDefinitionMap().get(name);
108  }
109
110  @SuppressWarnings("null")
111  @Override
112  public Collection<FI> getExportedFieldDefinitions() {
113    return getExports().getExportedFieldDefinitionMap().values();
114  }
115
116  @Override
117  public FI getExportedFieldDefinitionByName(Integer name) {
118    return getExports().getExportedFieldDefinitionMap().get(name);
119  }
120
121  @SuppressWarnings("null")
122  @Override
123  public Collection<A> getExportedAssemblyDefinitions() {
124    return getExports().getExportedAssemblyDefinitionMap().values();
125  }
126
127  @Override
128  public A getExportedAssemblyDefinitionByName(Integer name) {
129    return getExports().getExportedAssemblyDefinitionMap().get(name);
130  }
131
132  @Override
133  public A getExportedRootAssemblyDefinitionByName(Integer name) {
134    return getExports().getExportedRootAssemblyDefinitionMap().get(name);
135  }
136
137  @SuppressWarnings("unused") // used by lambda
138  private static <DEF extends IDefinition> DEF handleShadowedDefinitions(
139      @NonNull IEnhancedQName key,
140      @NonNull DEF oldDef,
141      @NonNull DEF newDef) {
142    if (!oldDef.equals(newDef) && LOGGER.isWarnEnabled()) {
143      LOGGER.warn("The {} '{}' from metaschema '{}' is shadowing '{}' from metaschema '{}'",
144          newDef.getModelType().name().toLowerCase(Locale.ROOT),
145          newDef.getName(),
146          newDef.getContainingModule().getShortName(),
147          oldDef.getName(),
148          oldDef.getContainingModule().getShortName());
149    }
150    return newDef;
151  }
152
153  @SuppressWarnings("unused") // used by lambda
154  private static <DEF extends IDefinition> DEF handleShadowedDefinitions(
155      @NonNull Integer key,
156      @NonNull DEF oldDef,
157      @NonNull DEF newDef) {
158    if (!oldDef.equals(newDef) && LOGGER.isWarnEnabled()) {
159      LOGGER.warn("The {} '{}' from metaschema '{}' is shadowing '{}' from metaschema '{}'",
160          newDef.getModelType().name().toLowerCase(Locale.ROOT),
161          newDef.getName(),
162          newDef.getContainingModule().getShortName(),
163          oldDef.getName(),
164          oldDef.getContainingModule().getShortName());
165    }
166    return newDef;
167  }
168
169  private class Exports {
170    @NonNull
171    private final Map<IEnhancedQName, FL> exportedFlagDefinitions;
172    @NonNull
173    private final Map<Integer, FI> exportedFieldDefinitions;
174    @NonNull
175    private final Map<Integer, A> exportedAssemblyDefinitions;
176    @NonNull
177    private final Map<Integer, A> exportedRootAssemblyDefinitions;
178
179    @SuppressWarnings({ "PMD.ConstructorCallsOverridableMethod", "synthetic-access" })
180    public Exports(@NonNull List<? extends M> importedModules) {
181      // Populate the stream with the definitions from this module
182      Predicate<IDefinition> filter = IModuleExtended.allNonLocalDefinitions();
183      Stream<FL> flags = getFlagDefinitions().stream()
184          .filter(filter);
185      Stream<FI> fields = getFieldDefinitions().stream()
186          .filter(filter);
187      Stream<A> assemblies = getAssemblyDefinitions().stream()
188          .filter(filter);
189
190      // handle definitions from any included module
191      if (!importedModules.isEmpty()) {
192        Stream<FL> importedFlags = Stream.empty();
193        Stream<FI> importedFields = Stream.empty();
194        Stream<A> importedAssemblies = Stream.empty();
195
196        for (M module : importedModules) {
197          importedFlags = Stream.concat(importedFlags, module.getExportedFlagDefinitions().stream());
198          importedFields = Stream.concat(importedFields, module.getExportedFieldDefinitions().stream());
199          importedAssemblies
200              = Stream.concat(importedAssemblies, module.getExportedAssemblyDefinitions().stream());
201        }
202
203        flags = Stream.concat(importedFlags, flags);
204        fields = Stream.concat(importedFields, fields);
205        assemblies = Stream.concat(importedAssemblies, assemblies);
206      }
207
208      // Build the maps. Definitions from this module will take priority, with
209      // shadowing being reported when a definition from this module has the same name
210      // as an imported one
211      Map<IEnhancedQName, FL> exportedFlagDefinitions = flags.collect(
212          CustomCollectors.toMap(
213              IFlagDefinition::getDefinitionQName,
214              CustomCollectors.identity(),
215              AbstractModule::handleShadowedDefinitions));
216      Map<Integer, FI> exportedFieldDefinitions = fields.collect(
217          CustomCollectors.toMap(
218              def -> def.getDefinitionQName().getIndexPosition(),
219              CustomCollectors.identity(),
220              AbstractModule::handleShadowedDefinitions));
221      Map<Integer, A> exportedAssemblyDefinitions = assemblies.collect(
222          CustomCollectors.toMap(
223              def -> def.getDefinitionQName().getIndexPosition(),
224              CustomCollectors.identity(),
225              AbstractModule::handleShadowedDefinitions));
226
227      this.exportedFlagDefinitions = exportedFlagDefinitions.isEmpty()
228          ? CollectionUtil.emptyMap()
229          : CollectionUtil.unmodifiableMap(exportedFlagDefinitions);
230      this.exportedFieldDefinitions = exportedFieldDefinitions.isEmpty()
231          ? CollectionUtil.emptyMap()
232          : CollectionUtil.unmodifiableMap(exportedFieldDefinitions);
233      this.exportedAssemblyDefinitions = exportedAssemblyDefinitions.isEmpty()
234          ? CollectionUtil.emptyMap()
235          : CollectionUtil.unmodifiableMap(exportedAssemblyDefinitions);
236      this.exportedRootAssemblyDefinitions = exportedAssemblyDefinitions.isEmpty()
237          ? CollectionUtil.emptyMap()
238          : CollectionUtil.unmodifiableMap(ObjectUtils.notNull(exportedAssemblyDefinitions.values().stream()
239              .filter(IAssemblyDefinition::isRoot)
240              .collect(CustomCollectors.toMap(
241                  def -> def.getRootQName().getIndexPosition(),
242                  CustomCollectors.identity(),
243                  AbstractModule::handleShadowedDefinitions))));
244    }
245
246    @NonNull
247    public Map<IEnhancedQName, FL> getExportedFlagDefinitionMap() {
248      return this.exportedFlagDefinitions;
249    }
250
251    @NonNull
252    public Map<Integer, FI> getExportedFieldDefinitionMap() {
253      return this.exportedFieldDefinitions;
254    }
255
256    @NonNull
257    public Map<Integer, A> getExportedAssemblyDefinitionMap() {
258      return this.exportedAssemblyDefinitions;
259    }
260
261    @NonNull
262    public Map<Integer, A> getExportedRootAssemblyDefinitionMap() {
263      return this.exportedRootAssemblyDefinitions;
264    }
265  }
266}