View Javadoc
1   /**
2    * This file Copyright (c) 2016-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.module.model.reader;
35  
36  import static java.nio.file.FileVisitOption.FOLLOW_LINKS;
37  
38  import info.magnolia.cms.util.ExceptionUtil;
39  import info.magnolia.init.MagnoliaConfigurationProperties;
40  import info.magnolia.map2bean.Map2BeanTransformer;
41  import info.magnolia.module.InstallContext;
42  import info.magnolia.module.ModuleManagementException;
43  import info.magnolia.module.ModuleVersionHandler;
44  import info.magnolia.module.delta.Delta;
45  import info.magnolia.module.delta.DeltaBuilder;
46  import info.magnolia.module.model.DependencyDefinition;
47  import info.magnolia.module.model.ModuleDefinition;
48  import info.magnolia.module.model.Version;
49  
50  import java.io.BufferedReader;
51  import java.io.IOException;
52  import java.io.InputStreamReader;
53  import java.io.Reader;
54  import java.nio.file.FileVisitResult;
55  import java.nio.file.Files;
56  import java.nio.file.Path;
57  import java.nio.file.Paths;
58  import java.nio.file.SimpleFileVisitor;
59  import java.nio.file.attribute.BasicFileAttributes;
60  import java.util.ArrayList;
61  import java.util.Collection;
62  import java.util.Collections;
63  import java.util.EnumSet;
64  import java.util.List;
65  import java.util.Map;
66  import java.util.regex.Matcher;
67  import java.util.regex.Pattern;
68  
69  import javax.inject.Inject;
70  import javax.inject.Singleton;
71  
72  import org.apache.commons.io.FilenameUtils;
73  import org.apache.commons.lang3.StringUtils;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  import org.yaml.snakeyaml.Yaml;
77  
78  import com.google.common.collect.ImmutableList;
79  import com.google.common.collect.Lists;
80  import com.google.common.collect.Maps;
81  
82  /**
83   * {@link LightModuleDefinitionReader} is capable of finding and reading the light module definitions. The following assumptions/requirements must hold for successful
84   * resolution of such definitions:
85   * <ul>
86   * <li>light module descriptor file must have the name <strong>module.yaml</strong></li>
87   * <li>light module descriptor file must contain YAML data which follows the structure of {@link LightModuleDefinition}</li>
88   * </ul>
89   *
90   * <strong>Note:</strong> unless specified explicitly the name of the light module is assigned with the name of the descriptor file's parent directory.
91   */
92  @Singleton
93  public class LightModuleDefinitionReader implements ModuleDefinitionReader {
94  
95      private static final Logger log = LoggerFactory.getLogger(LightModuleDefinitionReader.class);
96  
97      private static final String LIGHT_MODULE_DESCRIPTOR_NAME = "module.yaml";
98      private static final String MAGNOLIA_HOME = "magnolia.home";
99      private static final Pattern LIGHT_MODULE_DESCRIPTOR_PATH_PATTERN = Pattern.compile(String.format("^[\\w|/]*/(?<parent>\\w+?)/%s$", LIGHT_MODULE_DESCRIPTOR_NAME));
100 
101     private static final String RESOURCES_DIR_PROPERTY = "magnolia.resources.dir";
102     private final MagnoliaConfigurationProperties configurationProperties;
103     private final Map2BeanTransformer map2BeanTransformer;
104 
105     @Inject
106     public LightModuleDefinitionReader(MagnoliaConfigurationProperties configurationProperties, Map2BeanTransformer map2BeanTransformer) {
107         this.configurationProperties = configurationProperties;
108         this.map2BeanTransformer = map2BeanTransformer;
109     }
110 
111     @Override
112     public Map<String, ModuleDefinition> readAll() throws ModuleManagementException {
113         final Map<String, ModuleDefinition> moduleDefinitions = Maps.newHashMap();
114         final Collection<Path> lightModuleDescriptors;
115 
116         try {
117             lightModuleDescriptors = collectLightModuleDescriptorFiles();
118         } catch (IOException e) {
119             throw new ModuleManagementException("Encountered I/O issue during light module descriptors collection", e);
120         }
121 
122         for (final Path lightModuleDescriptor : lightModuleDescriptors) {
123             try (final Reader reader = Files.newBufferedReader(lightModuleDescriptor)) {
124                 final ModuleDefinition definition = read(reader, lightModuleDescriptor.getParent().getFileName().toString());
125                 moduleDefinitions.put(definition.getName(), definition);
126             } catch (Exception e) {
127                 throw new ModuleManagementException(String.format("Failed to read a light module descriptor at [%s]", lightModuleDescriptor), e);
128             }
129         }
130 
131         return moduleDefinitions;
132     }
133 
134     @Override
135     public ModuleDefinition read(Reader in) throws ModuleManagementException {
136         final ModuleDefinition moduleDefinition = read(in, "");
137         if (StringUtils.isBlank(moduleDefinition.getName())) {
138             throw new ModuleManagementException("Attempted to resolve light module definition via LightModuleDefinitionReader.read(java.io.Reader) without module name specified explicitly in the module descriptor file");
139         }
140         return moduleDefinition;
141     }
142 
143     @Override
144     public ModuleDefinition readFromResource(String resourcePath) throws ModuleManagementException {
145         final Matcher lightModuleDescriptorPathMatcher = LIGHT_MODULE_DESCRIPTOR_PATH_PATTERN.matcher(resourcePath);
146         if (lightModuleDescriptorPathMatcher.matches()) {
147             try (Reader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(resourcePath)))) {
148                 return read(reader, lightModuleDescriptorPathMatcher.group("parent"));
149             } catch (IOException e) {
150                 throw new ModuleManagementException(String.format("Failed to read a light module definition from the [%s]", resourcePath), e);
151             }
152         } else {
153             throw new ModuleManagementException(String.format("[%s] does not match a light module descriptor path pattern", resourcePath));
154         }
155     }
156 
157     private ModuleDefinition read(final Reader in, String nameFallback) {
158         final Object yaml = new Yaml().load(in);
159         if (!(yaml instanceof Map)) {
160             throw new IllegalArgumentException("Provided YAML stream does not yield a map");
161         }
162 
163         @SuppressWarnings("unchecked") Map<String, Object> yamlData = (Map<String, Object>) yaml;
164         try {
165             final LightModuleDefinition lightModuleDefinition = map2BeanTransformer.toBean(yamlData, LightModuleDefinition.class);
166             final ModuleDefinitiontml#ModuleDefinition">ModuleDefinition moduleDefinition = new ModuleDefinition();
167 
168             String name = lightModuleDefinition.getName();
169             if (StringUtils.isBlank(name)) {
170                 name = nameFallback;
171             }
172 
173             moduleDefinition.setVersion(lightModuleDefinition.getVersion());
174             moduleDefinition.setDependencies(ImmutableList.copyOf(lightModuleDefinition.getDependencies()));
175             moduleDefinition.setName(name);
176             moduleDefinition.setVersionHandler(LightModuleVersionHandler.class);
177 
178             return moduleDefinition;
179         } catch (Exception e) {
180             throw new RuntimeException("Failed to parse light module definition from YAML content stream", e);
181         }
182     }
183 
184     private Collection<Path> collectLightModuleDescriptorFiles() throws IOException {
185         final List<Path> lightModuleDescriptorPaths = Lists.newLinkedList();
186 
187         String resourcesDir = configurationProperties.getProperty(RESOURCES_DIR_PROPERTY);
188         if (resourcesDir == null) {
189             resourcesDir = configurationProperties.getProperty(MAGNOLIA_HOME);
190             log.warn("Could not find '{}' property, please update your 'magnolia.properties'. Falling back to '{}': '{}'.", RESOURCES_DIR_PROPERTY, MAGNOLIA_HOME, resourcesDir);
191         }
192 
193         final Path mgnlResourceDirectory = validateRootPath(resourcesDir);
194 
195         Files.walkFileTree(mgnlResourceDirectory, EnumSet.of(FOLLOW_LINKS), 2, new SimpleFileVisitor<Path>() {
196 
197             @Override
198             public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
199                 log.debug("Looking for [{}] within [{}]", LIGHT_MODULE_DESCRIPTOR_NAME, dir.getFileName());
200                 return super.preVisitDirectory(dir, attrs);
201             }
202 
203             @Override
204             public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
205                 if (LIGHT_MODULE_DESCRIPTOR_NAME.equals(file.getFileName().toString())) {
206                     log.debug("Encountered [{}] file inside of [{}]", LIGHT_MODULE_DESCRIPTOR_NAME, file.getParent().getFileName());
207                     lightModuleDescriptorPaths.add(file);
208                     return FileVisitResult.SKIP_SIBLINGS;
209                 }
210                 return FileVisitResult.CONTINUE;
211             }
212         });
213 
214         return lightModuleDescriptorPaths;
215     }
216 
217     /**
218      * Validate path is absolute and attempt to create a directory if it doesn't exist.
219      */
220     private Path validateRootPath(String path) {
221         Path rootPath = Paths.get(FilenameUtils.normalize(path));
222 
223         if (!rootPath.isAbsolute()) {
224             throw new IllegalStateException(rootPath + " is not an absolute Path.");
225         }
226 
227         if (!Files.exists(rootPath)) {
228             try {
229                 Files.createDirectories(rootPath);
230             } catch (Exception e) {
231                 throw new IllegalStateException("Tried to create " + rootPath + " and failed: " + ExceptionUtil.exceptionToWords(e), e);
232             }
233             log.info("Created {}", rootPath);
234         }
235 
236         return rootPath;
237     }
238 
239 
240     /**
241      * {@link DependencyDefinition} targeted for light module definitions.
242      *
243      * @see LightModuleDefinition
244      * @see info.magnolia.module.model.reader.LightModuleDefinitionReader
245      */
246     public static class LightModuleDependencyDefinition extends DependencyDefinition {
247 
248         public LightModuleDependencyDefinition() {
249         }
250     }
251 
252     /**
253      * Similar to {@link ModuleDefinition} this class supports only a sub-set of usual module definition properties.
254      * Used by {@link info.magnolia.module.model.reader.LightModuleDefinitionReader LightModuleDefinitionReader} as an
255      * intermediate layer during the conversion of the YAML data to module definition.
256      */
257     public static class LightModuleDefinition {
258 
259         private String name;
260 
261         private Version version;
262 
263         private Collection<LightModuleDependencyDefinition> dependencies = new ArrayList<>();
264 
265         public Version getVersion() {
266             return version;
267         }
268 
269         public void setVersion(Version version) {
270             this.version = version;
271         }
272 
273         public Collection<LightModuleDependencyDefinition> getDependencies() {
274             return dependencies;
275         }
276 
277         public void setDependencies(Collection<LightModuleDependencyDefinition> dependencies) {
278             this.dependencies = dependencies;
279         }
280 
281         public String getName() {
282             return name;
283         }
284 
285         public void setName(String name) {
286             this.name = name;
287         }
288     }
289 
290     /**
291      * {@link ModuleVersionHandler} that doesn't do a thing, not even register a module version in JCR. This is what we
292      * want because light modules are volatile and shouldn't clutter the repository.
293      */
294     public static final class LightModuleVersionHandler implements ModuleVersionHandler {
295 
296         @Override
297         public Version getCurrentlyInstalled(InstallContext ctx) {
298             return ctx.getCurrentModuleDefinition().getVersion();
299         }
300 
301         @Override
302         public List<Delta> getDeltas(InstallContext installContext, Version from) {
303             return Collections.emptyList();
304         }
305 
306         @Override
307         public Delta getStartupDelta(InstallContext ctx) {
308             return DeltaBuilder.startup(ctx.getCurrentModuleDefinition(), Collections.emptyList());
309         }
310     }
311 }