View Javadoc
1   /**
2    * This file Copyright (c) 2017-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.config.source.yaml.construct;
35  
36  import static info.magnolia.config.source.yaml.dependency.YamlDependencyResoutionProblemType.DEPENDENCY_RESOLUTION;
37  import static java.util.stream.Collectors.toList;
38  
39  import info.magnolia.config.maputil.ConfigurationMapOverlay;
40  import info.magnolia.config.maputil.ToMap;
41  import info.magnolia.config.registry.DefinitionProvider.Problem;
42  import info.magnolia.config.registry.Registry;
43  import info.magnolia.config.source.yaml.MgnlYamlConstructor;
44  import info.magnolia.config.source.yaml.dependency.DefinitionDependency;
45  import info.magnolia.module.ModuleRegistry;
46  import info.magnolia.module.model.DependencyDefinition;
47  
48  import java.util.Collection;
49  import java.util.Collections;
50  import java.util.Map;
51  import java.util.function.Consumer;
52  import java.util.regex.Matcher;
53  import java.util.regex.Pattern;
54  import java.util.stream.Collectors;
55  
56  import org.apache.commons.lang3.StringUtils;
57  import org.yaml.snakeyaml.nodes.Node;
58  import org.yaml.snakeyaml.nodes.ScalarNode;
59  
60  /**
61   * SnakeYAML construct bound to the <b>!inherit:</b>-prefixed tags. The tag value has to consist of three parts in total
62   * (e.g. <b>!inherit:app:fooApp</b>):
63   * <ul>
64   * <li>the aforementioned prefix, which is always the same;</li>
65   * <li>the registry identifier (has to correspond the {@link info.magnolia.config.registry.DefinitionType#getName()} return value);</li>
66   * <li>the definition reference id.</li>
67   * </ul>
68   *
69   * The referenced definition data is pulled from the registry and then the value (map) parsed from the current YAML node is
70   * combined with that data.
71   *
72   * @param <T> inherited definition type
73   */
74  public final class InheritDefinition<T> extends MgnlYamlConstruct {
75  
76      public static final String TAG_PREFIX = "!inherit:";
77  
78      private static final Pattern INHERITANCE_PATTERN = Pattern.compile("^!inherit:(?<definitionreference>.+)$");
79  
80      private final Registry<T> registry;
81      private final ModuleRegistry moduleRegistry;
82  
83      private final String sourceModule;
84      private final String defaultName;
85  
86      public InheritDefinition(Registry<T> targetRegistry, ModuleRegistry moduleRegistry, String currentModule, String defaultName, Consumer<Problem> problemCollector) {
87          super(problemCollector);
88          registry = targetRegistry;
89          this.moduleRegistry = moduleRegistry;
90          this.sourceModule = currentModule;
91          this.defaultName = defaultName;
92      }
93  
94      @Override
95      public Object construct(Node node) {
96          final MgnlYamlConstructor constructor = getConstructor();
97          final Map<String, Object> baseData = ToMap.toMap(constructor.getConstructByNodeType(node).construct(node));
98  
99          if (node instanceof ScalarNode) {
100             final String scalarValue = ((ScalarNode) node).getValue();
101             if (!StringUtils.isEmpty(scalarValue)) {
102                 reportProblem(
103                     Problem
104                         .minor()
105                         .withType(DEPENDENCY_RESOLUTION)
106                         .withTitle("Redundant inheritance syntax used")
107                         .withDetails(String.format("Scalar value [%s] of node [%s] will be ignored due to !inherit semantics",
108                                 scalarValue, node.getNodeId()))
109                         .build());
110             }
111         }
112 
113         final Matcher definitionReferenceMatcher = INHERITANCE_PATTERN.matcher(node.getTag().getValue());
114         if (!definitionReferenceMatcher.matches()) {
115             reportProblem(
116                 Problem
117                     .severe()
118                     .withType(DEPENDENCY_RESOLUTION)
119                     .withTitle("Mis-configured definition dependency")
120                     .withDetails(String.format("Tag [%s] does not match the inheritance pattern !inherit:<type>:<referenceId>",
121                             node.getTag().getValue()))
122                     .build());
123             return baseData;
124         }
125 
126         final String definitionReference = definitionReferenceMatcher.group("definitionreference");
127         final DefinitionDependency dependency =
128                 DefinitionDependency
129                         .resolve()
130                         .withRegistry(registry)
131                         .withDefinitionReference(definitionReference)
132                         .withProblemCollector(this::reportProblem)
133                         .build();
134 
135         constructor.getDependencyAggregator().addDependency(dependency);
136 
137         if (!dependency.exists()) {
138             final String moduleDependencies = getSourceModuleDependencies().stream().collect(Collectors.joining(", "));
139             reportProblem(
140                 Problem
141                     .severe()
142                     .withType(DEPENDENCY_RESOLUTION)
143                     .withTitle("Missing definition dependency")
144                     .withDetails(String.format("Inherited definition at [%s] either does not exist or is not loaded because of the missing module dependency in module [%s], which currently depends only on [%s]",
145                             definitionReference, sourceModule, moduleDependencies))
146                     .build());
147             // since the dependency does not exist at all - do not attempt to resolve it (it'll blow up!), merely return the data as if there is no dependency.
148             return baseData;
149         }
150 
151         analyseModuleDependencyCorrectness(definitionReference, dependency);
152 
153         /*
154          * Since we are dealing with one definition inheriting the other one,
155          * name property is something really sensitive (you usually do not want
156          * the name also be inherited because that might lead to issues, like e.g.
157          * two apps with the same name discovered by the system and only one of
158          * them will get registered). Instead, we want to give the heir definition
159          * a chance to provide its own default name (e.g. the name of the related YAML
160          * file name) which is used instead of the inherited name.
161          * NOTE: the names of the sub-definitions are inherited just fine.
162          */
163         baseData.putIfAbsent("name", defaultName);
164 
165         return ConfigurationMapOverlay
166                 .of(dependency.readData())
167                 .by(baseData)
168                 .at("/")
169                 .overlay();
170 
171     }
172 
173     private void analyseModuleDependencyCorrectness(String definitionReference, DefinitionDependency dependency) {
174         final String dependencyModule = dependency.resolveDefinitionProvider().getMetadata().getModule();
175 
176         String moduleDependencyProblemStatement = null;
177         if (StringUtils.equals(dependencyModule, sourceModule)) {
178             moduleDependencyProblemStatement =
179                     String.format("Dependency definition [%s] resides at the same module [%s] as the dependent one, which might lead to inconsistencies",
180                             definitionReference, dependencyModule);
181         } else if (isUndeclaredModuleDependency(sourceModule, dependencyModule)) {
182             moduleDependencyProblemStatement =
183                     String.format("Dependency definition [%s] resides at the module [%s] not dependent by the source definition's module [%s]",
184                             definitionReference, dependencyModule, sourceModule);
185         }
186 
187         if (StringUtils.isNotBlank(moduleDependencyProblemStatement)) {
188             reportProblem(
189                 Problem
190                     .major()
191                     .withType(DEPENDENCY_RESOLUTION)
192                     .withTitle("Definition inherited with module dependency problem")
193                     .withDetails(moduleDependencyProblemStatement)
194                     .build());
195         }
196     }
197 
198     private Collection<String> getSourceModuleDependencies() {
199         if (this.moduleRegistry.isModuleRegistered(this.sourceModule)) {
200             return this.moduleRegistry.getDefinition(this.sourceModule)
201                     .getDependencies().stream()
202                     .map(DependencyDefinition::getName)
203                     .collect(toList());
204         }
205         return Collections.emptySet();
206     }
207 
208     private boolean isUndeclaredModuleDependency(String currentModule, String dependencyModule) {
209         return !moduleRegistry.isModuleRegistered(currentModule) ||
210                 moduleRegistry
211                     .getDefinition(currentModule)
212                     .getDependencies()
213                     .stream()
214                     .map(DependencyDefinition::getName)
215                     .noneMatch(name -> name.equals(dependencyModule));
216     }
217 }