Bug 1331 - Generate SPIs and yangs to target/generated-sources/ subfolders
[yangtools.git] / yang / yang-maven-plugin / src / main / java / org / opendaylight / yangtools / yang2sources / plugin / Util.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang2sources.plugin;
9
10 import java.io.Closeable;
11 import java.io.File;
12 import java.io.FileNotFoundException;
13 import java.io.FilenameFilter;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.Enumeration;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.zip.ZipEntry;
25 import java.util.zip.ZipFile;
26
27 import org.apache.commons.io.FileUtils;
28 import org.apache.maven.artifact.Artifact;
29 import org.apache.maven.artifact.repository.ArtifactRepository;
30 import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
31 import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
32 import org.apache.maven.model.Dependency;
33 import org.apache.maven.model.Plugin;
34 import org.apache.maven.plugin.MojoFailureException;
35 import org.apache.maven.plugin.logging.Log;
36 import org.apache.maven.project.MavenProject;
37 import org.opendaylight.yangtools.yang.model.api.Module;
38 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
39 import org.opendaylight.yangtools.yang.parser.util.NamedFileInputStream;
40 import org.apache.maven.repository.RepositorySystem;
41
42 import com.google.common.base.Preconditions;
43 import com.google.common.collect.Lists;
44 import com.google.common.collect.Maps;
45
46 final class Util {
47
48     /**
49      * It isn't desirable to create instances of this class
50      */
51     private Util() {
52     }
53
54     static final String YANG_SUFFIX = "yang";
55
56     private static final int CACHE_SIZE = 10;
57     // Cache for listed directories and found yang files. Typically yang files
58     // are utilized twice. First: code is generated during generate-sources
59     // phase Second: yang files are copied as resources during
60     // generate-resources phase. This cache ensures that yang files are listed
61     // only once.
62     private static Map<File, Collection<File>> cache = Maps.newHashMapWithExpectedSize(CACHE_SIZE);
63
64     /**
65      * List files recursively and return as array of String paths. Use cache of
66      * size 1.
67      */
68     static Collection<File> listFiles(File root) throws FileNotFoundException {
69         if (cache.get(root) != null) {
70             return cache.get(root);
71         }
72
73         if (!root.exists()) {
74             throw new FileNotFoundException(root.toString());
75         }
76
77         Collection<File> yangFiles = FileUtils.listFiles(root, new String[] { YANG_SUFFIX }, true);
78
79         toCache(root, yangFiles);
80         return yangFiles;
81     }
82
83     static Collection<File> listFiles(File root, File[] excludedFiles, Log log) throws FileNotFoundException {
84         if (!root.exists()) {
85             throw new FileNotFoundException(root.toString());
86         }
87         Collection<File> result = new ArrayList<>();
88         Collection<File> yangFiles = FileUtils.listFiles(root, new String[] { YANG_SUFFIX }, true);
89         for (File f : yangFiles) {
90             boolean excluded = false;
91             for (File ex : excludedFiles) {
92                 if (ex.equals(f)) {
93                     excluded = true;
94                     break;
95                 }
96             }
97             if (excluded) {
98                 if (log != null) {
99                     log.info(Util.message("%s file excluded %s", YangToSourcesProcessor.LOG_PREFIX,
100                             Util.YANG_SUFFIX.toUpperCase(), f));
101                 }
102             } else {
103                 result.add(f);
104             }
105         }
106
107         return result;
108     }
109
110     private static void toCache(final File rootDir, final Collection<File> yangFiles) {
111         cache.put(rootDir, yangFiles);
112     }
113
114     /**
115      * Instantiate object from fully qualified class name
116      */
117     static <T> T getInstance(String codeGeneratorClass, Class<T> baseType) throws ClassNotFoundException,
118             InstantiationException, IllegalAccessException {
119         return baseType.cast(resolveClass(codeGeneratorClass, baseType).newInstance());
120     }
121
122     private static Class<?> resolveClass(String codeGeneratorClass, Class<?> baseType) throws ClassNotFoundException {
123         Class<?> clazz = Class.forName(codeGeneratorClass);
124
125         if (!isImplemented(baseType, clazz)) {
126             throw new IllegalArgumentException("Code generator " + clazz + " has to implement " + baseType);
127         }
128         return clazz;
129     }
130
131     private static boolean isImplemented(Class<?> expectedIface, Class<?> byClazz) {
132         for (Class<?> iface : byClazz.getInterfaces()) {
133             if (iface.equals(expectedIface)) {
134                 return true;
135             }
136         }
137         return false;
138     }
139
140     static String message(String message, String logPrefix, Object... args) {
141         String innerMessage = String.format(message, args);
142         return String.format("%s %s", logPrefix, innerMessage);
143     }
144
145     static List<File> getClassPath(MavenProject project) {
146         List<File> dependencies = Lists.newArrayList();
147         for (Artifact element : project.getArtifacts()) {
148             File asFile = element.getFile();
149             if (isJar(asFile) || asFile.isDirectory()) {
150                 dependencies.add(asFile);
151             }
152         }
153         return dependencies;
154     }
155
156     /**
157      * Read current project dependencies and check if it don't grab incorrect
158      * artifacts versions which could be in conflict with plugin dependencies.
159      *
160      * @param project
161      *            current project
162      * @param repoSystem
163      *            repository system
164      * @param localRepo
165      *            local repository
166      * @param remoteRepos
167      *            remote repositories
168      * @param log
169      *            logger
170      */
171     static void checkClasspath(MavenProject project, RepositorySystem repoSystem, ArtifactRepository localRepo,
172             List<ArtifactRepository> remoteRepos, Log log) {
173         Plugin plugin = project.getPlugin(YangToSourcesMojo.PLUGIN_NAME);
174         if (plugin == null) {
175             log.warn(message("%s not found, dependencies version check skipped", YangToSourcesProcessor.LOG_PREFIX,
176                     YangToSourcesMojo.PLUGIN_NAME));
177         } else {
178             Map<Artifact, Collection<Artifact>> pluginDependencies = new HashMap<>();
179             getPluginTransitiveDependencies(plugin, pluginDependencies, repoSystem, localRepo, remoteRepos, log);
180
181             Set<Artifact> projectDependencies = project.getDependencyArtifacts();
182             for (Map.Entry<Artifact, Collection<Artifact>> entry : pluginDependencies.entrySet()) {
183                 checkArtifact(entry.getKey(), projectDependencies, log);
184                 for (Artifact dependency : entry.getValue()) {
185                     checkArtifact(dependency, projectDependencies, log);
186                 }
187             }
188         }
189     }
190
191     /**
192      * Read transitive dependencies of given plugin and store them in map.
193      *
194      * @param plugin
195      *            plugin to read
196      * @param map
197      *            map, where founded transitive dependencies will be stored
198      * @param repoSystem
199      *            repository system
200      * @param localRepository
201      *            local repository
202      * @param remoteRepos
203      *            list of remote repositories
204      * @param log
205      *            logger
206      */
207     private static void getPluginTransitiveDependencies(Plugin plugin, Map<Artifact, Collection<Artifact>> map,
208             RepositorySystem repoSystem, ArtifactRepository localRepository, List<ArtifactRepository> remoteRepos,
209             Log log) {
210
211         List<Dependency> pluginDependencies = plugin.getDependencies();
212         for (Dependency dep : pluginDependencies) {
213             Artifact artifact = repoSystem.createDependencyArtifact(dep);
214
215             ArtifactResolutionRequest request = new ArtifactResolutionRequest();
216             request.setArtifact(artifact);
217             request.setResolveTransitively(true);
218             request.setLocalRepository(localRepository);
219             request.setRemoteRepositories(remoteRepos);
220
221             ArtifactResolutionResult result = repoSystem.resolve(request);
222             Set<Artifact> pluginDependencyDependencies = result.getArtifacts();
223             map.put(artifact, pluginDependencyDependencies);
224         }
225     }
226
227     /**
228      * Check artifact against collection of dependencies. If collection contains
229      * artifact with same groupId and artifactId, but different version, logs a
230      * warning.
231      *
232      * @param artifact
233      *            artifact to check
234      * @param dependencies
235      *            collection of dependencies
236      * @param log
237      *            logger
238      */
239     private static void checkArtifact(Artifact artifact, Collection<Artifact> dependencies, Log log) {
240         for (org.apache.maven.artifact.Artifact d : dependencies) {
241             if (artifact.getGroupId().equals(d.getGroupId()) && artifact.getArtifactId().equals(d.getArtifactId())) {
242                 if (!(artifact.getVersion().equals(d.getVersion()))) {
243                     log.warn(message("Dependency resolution conflict:", YangToSourcesProcessor.LOG_PREFIX));
244                     log.warn(message("'%s' dependency [%s] has different version than one "
245                             + "declared in current project [%s]. It is recommended to fix this problem "
246                             + "because it may cause compilation errors.", YangToSourcesProcessor.LOG_PREFIX,
247                             YangToSourcesMojo.PLUGIN_NAME, artifact, d));
248                 }
249             }
250         }
251     }
252
253     private static final String JAR_SUFFIX = ".jar";
254
255     private static boolean isJar(File element) {
256         return (element.isFile() && element.getName().endsWith(JAR_SUFFIX)) ? true : false;
257     }
258
259     static <T> T checkNotNull(T obj, String paramName) {
260         return Preconditions.checkNotNull(obj, "Parameter " + paramName + " is null");
261     }
262
263     static final class YangsInZipsResult implements Closeable {
264         private final List<InputStream> yangStreams;
265         private final List<Closeable> zipInputStreams;
266
267         private YangsInZipsResult(List<InputStream> yangStreams, List<Closeable> zipInputStreams) {
268             this.yangStreams = yangStreams;
269             this.zipInputStreams = zipInputStreams;
270         }
271
272         @Override
273         public void close() throws IOException {
274             for (InputStream is : yangStreams) {
275                 is.close();
276             }
277             for (Closeable is : zipInputStreams) {
278                 is.close();
279             }
280         }
281
282         public List<InputStream> getYangStreams() {
283             return this.yangStreams;
284         }
285     }
286
287     static YangsInZipsResult findYangFilesInDependenciesAsStream(Log log, MavenProject project)
288             throws MojoFailureException {
289         List<InputStream> yangsFromDependencies = new ArrayList<>();
290         List<Closeable> zips = new ArrayList<>();
291         try {
292             List<File> filesOnCp = Util.getClassPath(project);
293             log.info(Util.message("Searching for yang files in following dependencies: %s",
294                     YangToSourcesProcessor.LOG_PREFIX, filesOnCp));
295
296             for (File file : filesOnCp) {
297                 List<String> foundFilesForReporting = new ArrayList<>();
298                 // is it jar file or directory?
299                 if (file.isDirectory()) {
300                     //FIXME: code duplicate
301                     File yangDir = new File(file, YangToSourcesProcessor.META_INF_YANG_STRING);
302                     if (yangDir.exists() && yangDir.isDirectory()) {
303                         File[] yangFiles = yangDir.listFiles(new FilenameFilter() {
304                             @Override
305                             public boolean accept(File dir, String name) {
306                                 return name.endsWith(".yang") && new File(dir, name).isFile();
307                             }
308                         });
309                         for (File yangFile : yangFiles) {
310                             yangsFromDependencies.add(new NamedFileInputStream(yangFile, YangToSourcesProcessor.META_INF_YANG_STRING + File.separator + yangFile.getName()));
311                         }
312                     }
313
314                 } else {
315                     ZipFile zip = new ZipFile(file);
316                     zips.add(zip);
317
318                     Enumeration<? extends ZipEntry> entries = zip.entries();
319                     while (entries.hasMoreElements()) {
320                         ZipEntry entry = entries.nextElement();
321                         String entryName = entry.getName();
322
323                         if (entryName.startsWith(YangToSourcesProcessor.META_INF_YANG_STRING_JAR)
324                                 && !entry.isDirectory() && entryName.endsWith(".yang")) {
325                             foundFilesForReporting.add(entryName);
326                             // This will be closed after all streams are
327                             // parsed.
328                             InputStream entryStream = zip.getInputStream(entry);
329                             yangsFromDependencies.add(entryStream);
330                         }
331                     }
332                 }
333                 if (foundFilesForReporting.size() > 0) {
334                     log.info(Util.message("Found %d yang files in %s: %s", YangToSourcesProcessor.LOG_PREFIX,
335                             foundFilesForReporting.size(), file, foundFilesForReporting));
336                 }
337
338             }
339         } catch (Exception e) {
340             throw new MojoFailureException(e.getMessage(), e);
341         }
342         return new YangsInZipsResult(yangsFromDependencies, zips);
343     }
344
345     static Collection<File> findYangFilesInDependencies(Log log, MavenProject project) throws MojoFailureException {
346         final List<File> yangsFilesFromDependencies = new ArrayList<>();
347
348         try {
349             List<File> filesOnCp = Util.getClassPath(project);
350             log.info(Util.message("Searching for yang files in following dependencies: %s",
351                     YangToSourcesProcessor.LOG_PREFIX, filesOnCp));
352
353             for (File file : filesOnCp) {
354                 // is it jar file or directory?
355                 if (file.isDirectory()) {
356                     //FIXME: code duplicate
357                     File yangDir = new File(file, YangToSourcesProcessor.META_INF_YANG_STRING);
358                     if (yangDir.exists() && yangDir.isDirectory()) {
359                         File[] yangFiles = yangDir.listFiles(new FilenameFilter() {
360                             @Override
361                             public boolean accept(File dir, String name) {
362                                 return name.endsWith(".yang") && new File(dir, name).isFile();
363                             }
364                         });
365
366                         yangsFilesFromDependencies.addAll(Arrays.asList(yangFiles));
367                     }
368                 } else {
369                     try (ZipFile zip = new ZipFile(file)) {
370
371                         final Enumeration<? extends ZipEntry> entries = zip.entries();
372                         while (entries.hasMoreElements()) {
373                             ZipEntry entry = entries.nextElement();
374                             String entryName = entry.getName();
375
376                             if (entryName.startsWith(YangToSourcesProcessor.META_INF_YANG_STRING_JAR)
377                                     && !entry.isDirectory() && entryName.endsWith(".yang")) {
378                                 log.debug(Util.message("Found a YANG file in %s: %s", YangToSourcesProcessor.LOG_PREFIX,
379                                         file, entryName));
380                                 yangsFilesFromDependencies.add(file);
381                                 break;
382                             }
383                         }
384                     }
385                 }
386             }
387         } catch (Exception e) {
388             throw new MojoFailureException("Failed to scan for YANG files in depedencies", e);
389         }
390         return yangsFilesFromDependencies;
391     }
392
393     static final class ContextHolder {
394         private final SchemaContext context;
395         private final Set<Module> yangModules;
396
397         ContextHolder(SchemaContext context, Set<Module> yangModules) {
398             this.context = context;
399             this.yangModules = yangModules;
400         }
401
402         SchemaContext getContext() {
403             return context;
404         }
405
406         Set<Module> getYangModules() {
407             return yangModules;
408         }
409     }
410
411 }