CodeCoverage improvement for dcaegen2-platform-mod-genprocessor
[dcaegen2/platform.git] / mod / genprocessor / src / main / java / org / onap / dcae / genprocessor / App.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
4  * Copyright (C) 2022 Huawei. All rights reserved.
5  * ================================================================================
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  * 
10  *      http://www.apache.org/licenses/LICENSE-2.0
11  * 
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  * ============LICENSE_END=========================================================
18  */
19 package org.onap.dcae.genprocessor;
20
21 import javassist.CannotCompileException;
22 import javassist.ClassPool;
23 import javassist.CtClass;
24
25 import java.io.File;
26 import java.io.FilenameFilter;
27 import java.io.InputStream;
28 import java.io.IOException;
29 import java.net.MalformedURLException;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.net.URL;
33 import java.net.URLClassLoader;
34 import java.nio.charset.StandardCharsets;
35 import java.nio.file.FileAlreadyExistsException;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.util.Arrays;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.ServiceLoader;
43 import java.util.jar.Manifest;
44 import java.util.stream.Collectors;
45
46 import com.fasterxml.jackson.core.JsonFactory;
47 import com.fasterxml.jackson.databind.ObjectMapper;
48
49 import java.util.jar.Attributes;
50
51 import org.apache.nifi.annotation.documentation.CapabilityDescription;
52 import org.apache.nifi.annotation.documentation.Tags;
53 import org.apache.nifi.processor.Processor;
54
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * Hello world!
60  *
61  */
62 public class App {
63     static final Logger LOG = LoggerFactory.getLogger(App.class);
64
65     // NOTE: For each new processor, need to: change jar command, change meta-inf
66     private static String createClassName(CompSpec compSpec) {
67         return String.format("org.onap.dcae.%s", compSpec.nameJavaClass);
68     }
69
70     /**
71      * Does a series of DCAEProcessor specific verification checks on the generated
72      * class.
73      * 
74      * @param cc
75      * @return true if verification is successful
76      */
77     private static boolean verifyGen(CtClass cc) {
78         DCAEProcessor processor;
79         try {
80             processor = (DCAEProcessor) cc.toClass().newInstance();
81         } catch (InstantiationException | IllegalAccessException | CannotCompileException e) {
82             LOG.error(e.toString(), e);
83             return false;
84         }
85         java.lang.annotation.Annotation[] anns = processor.getClass().getAnnotations();
86         LOG.info(String.format("#Annotations on class: %d", anns.length));
87
88         for (java.lang.annotation.Annotation ann : anns) {
89             if (ann.annotationType().getName().contains("Description")) {
90                 LOG.info(String.format("CapabilityDescription: %s", ((CapabilityDescription) ann).value()));
91             } else if (ann.annotationType().getName().contains("Tags")) {
92                 LOG.info(String.format("Tags: %s", String.join(", ", ((Tags) ann).value())));
93             }
94         }
95
96         LOG.info(String.format("Processor getters:\nname=%s\nversion=%s\nid=%s\nselfUrl=%s", processor.getName(),
97                 processor.getVersion(), processor.getComponentId(), processor.getComponentUrl()));
98
99         LOG.info(String.format("#Property descriptors: %d", processor.getPropertyDescriptors().size()));
100
101         if (processor.getPropertyDescriptors().size() > 0) {
102             LOG.info(processor.getPropertyDescriptors().get(0).toString());
103         }
104
105         LOG.info(String.format("#Relationships: %d", processor.getRelationships().size()));
106
107         // Actually do checks
108         return true;
109     }
110
111     /**
112      * Generates a new DCAEProcessor class given a Component object and writes the
113      * class file to a specified directory.
114      * 
115      * @param directoryName where generated class files will get written
116      * @param comp          Component object to generate new DCAEProcessor classes
117      */
118     public static void generateClassFile(String directoryName, Comp comp) {
119         LOG.info("Generating classes");
120
121         try {
122             ClassPool pool = ClassPool.getDefault();
123             CtClass base = pool.get(DCAEProcessor.class.getName());
124
125             CtClass cc = pool.makeClass(createClassName(comp.compSpec));
126             cc.setSuperclass(base);
127
128             String[] tags = ProcessorBuilder.createTags(comp.compSpec);
129
130             ProcessorBuilder.addAnnotationsProcessor(cc, comp.compSpec.description, tags);
131             ProcessorBuilder.setComponentPropertyGetters(cc, comp);
132             ProcessorBuilder.setProcessorPropertyDescriptors(cc, comp.compSpec);
133             ProcessorBuilder.setProcessorRelationships(cc, comp.compSpec);
134
135             if (verifyGen(cc)) {
136                 cc.writeFile(directoryName);
137             }
138         } catch (Exception e) {
139             LOG.error("Uhoh", e);
140         }
141     }
142
143     private static List<String> generateManifestMF(CompSpec compSpec) {
144         return Arrays.asList("Manifest-Version: 1.0", String.format("Id: %s", compSpec.name),
145                 String.format("Version: %s", compSpec.version)
146                 // TODO: Group is hardcoded here. Should it be?
147                 , "Group: org.onap.dcae");
148     }
149
150     private static void writeManifestThing(File dirTarget, List<String> lines, String subDir, String fileName) {
151         File dirManifest = new File(dirTarget, subDir);
152
153         if (dirManifest.exists() || dirManifest.mkdirs()) {
154             Path path = Paths.get(dirManifest.getAbsolutePath(), fileName);
155             try {
156                 Files.write(path, lines, StandardCharsets.UTF_8);
157             } catch (IOException e) {
158                 throw new RuntimeException(e);
159             }
160         } else {
161             throw new RuntimeException("Could not create Manifest directory");
162         }
163     }
164
165     private static boolean copyProcessorClassFile(String classResourceName, File dirBuild) {
166         File dirSandbox = new File(dirBuild, "org/onap/dcae/genprocessor");
167
168         if (dirSandbox.exists() || dirSandbox.mkdir()) {
169             try (InputStream asStream = App.class.getResourceAsStream(classResourceName)) {
170                 File dest = new File(dirSandbox, classResourceName);
171                 Files.copy(asStream, dest.toPath());
172                 return true;
173             } catch (FileAlreadyExistsException e) {
174                 // Do nothing, class file already exists
175             } catch (IOException e) {
176                 throw new RuntimeException(e);
177             } 
178         }
179
180         return false;
181     }
182
183     private static File getDirectoryForJars(File dirWorking) {
184         return new File(dirWorking, "nifi-jars");
185     }
186
187     private static boolean packageJar(File dirWorking, File dirBuild, String jarName) {
188         LOG.info("Package into jar");
189
190         try {
191             File dirJars = getDirectoryForJars(dirWorking);
192
193             if (dirJars.exists() || dirJars.mkdir()) {
194                 String argDashC = String.format("-C %s", dirBuild.getAbsolutePath());
195                 String cmd = String.join(" ", new String[] {
196                     "jar cvfm"
197                     , String.format("%s/%s.jar", dirJars.getAbsolutePath(), jarName)
198                     , String.format("%s/META-INF/MANIFEST.MF", dirBuild.getAbsolutePath())
199                     , argDashC, "org"
200                     , argDashC, "org/onap/dcae/genprocessor"
201                     , argDashC, "META-INF"
202                 });
203                 LOG.debug(String.format("Jar command: %s", cmd));
204
205                 if (Runtime.getRuntime().exec(cmd).waitFor() == 0) {
206                     return true;
207                 }
208             }
209         } catch (InterruptedException | IOException e) {
210             throw new RuntimeException("Error while creating jar", e);
211         }
212
213         return false;
214     }
215
216     private static boolean doesJarExist(File dirWorking, String jarName) {
217         File dirJars = getDirectoryForJars(dirWorking);
218         String[] jars = dirJars.list(new FilenameFilter() {
219             @Override
220             public boolean accept(File dir, String name) {
221                 return name.contains(jarName);
222             }
223         });
224         return jars.length > 0;
225     }
226
227     /**
228      * Looks for the MANIFEST.MF and extracts-to-print expected values (group, id,
229      * version)
230      * 
231      * @param classLoader
232      */
233     private static void checkManifest(ClassLoader classLoader) {
234         try {
235             URL url = ((URLClassLoader) classLoader).findResource("META-INF/MANIFEST.MF");
236             Manifest manifest = new Manifest(url.openStream());
237
238             final Attributes attributes = manifest.getMainAttributes();
239             final String group = attributes.getValue("Group");
240             final String id = attributes.getValue("Id");
241             final String version = attributes.getValue("Version");
242             LOG.info(String.format("group=%s, id=%s, version=%s", group, id, version));
243         } catch (IOException e) {
244             throw new RuntimeException("Error while reading manifest", e);
245         }
246     }
247
248     /**
249      * Given a URL to a index.json file, fetches the file and generates a list of
250      * URLs for DCAE jars that has Processors packaged.
251      * 
252      * @param indexDCAEJars
253      * @return
254      */
255     public static List<URL> getDCAEJarsURLs(URI indexDCAEJars) {
256         JsonFactory jf = new JsonFactory();
257         ObjectMapper om = new ObjectMapper();
258
259         try {
260             List<Object> urls = om.readValue(jf.createParser(indexDCAEJars.toURL()), List.class);
261
262             return urls.stream().map(u -> {
263                 try {
264                     Map<String, Object> foo = (Map<String, Object>) u;
265                     String name = (String) foo.get("name");
266                     String url = String.format("%s/%s", indexDCAEJars.toString(), name);
267                     return new URL(url);
268                 } catch (MalformedURLException e) {
269                     // Hopefully you never come here...
270                     return null;
271                 }
272             }).collect(Collectors.toList());
273         } catch (Exception e) {
274             throw new RuntimeException("Error while getting jar URIs", e);
275         }
276     }
277
278     /**
279      * Loads all the Processor classes from the list of jar URLs and does a
280      * validation check that prints to screen.
281      * 
282      * @param jarURLs
283      */
284     public static void loadFromJars(URL[] jarURLs) {
285         URLClassLoader urlClassLoader = new URLClassLoader(jarURLs);
286         checkManifest(urlClassLoader);
287
288         final ServiceLoader<?> serviceLoader = ServiceLoader.load(Processor.class, urlClassLoader);
289
290         for (final Object o : serviceLoader) {
291             LOG.info(o.getClass().getName());
292             DCAEProcessor proc = ((DCAEProcessor) o);
293             proc.ping();
294             LOG.info(String.format("%s: %s", proc.getName(), proc.getComponentUrl()));
295         }
296
297         // TODO: Can fetch the comp spec with the component url to do further
298         // validation..
299     }
300
301     private static boolean init(File dirWorking) {
302         File dirJars = getDirectoryForJars(dirWorking);
303
304         if (dirJars.exists() || dirJars.mkdirs()) {
305             return true;
306         }
307
308         return false;
309     }
310
311     public static void main(String[] args) throws InterruptedException, URISyntaxException {
312         if (args.length == 0) {
313             args = new String[] { "gen" };
314             String sleepstr = System.getenv("GENPROC_SLEEP_SEC");
315             long sleepdur = (sleepstr != null)? 1000 * Long.parseLong(sleepstr): 0;
316             do {
317                 try {
318                     main2(args);
319                 } catch (Exception e) {
320                     LOG.error(e.toString(), e);
321                 }
322                 Thread.sleep(sleepdur);
323             } while (sleepdur > 0);
324             return;
325         } else {
326             main2(args);
327         }
328     }
329
330
331     public static void main2(String[] args) throws URISyntaxException {
332         String argsStr = String.join(", ", args);
333         if (argsStr.contains("-h")) {
334             LOG.info("Here are the possible args:");
335             LOG.info("<gen> <load>");
336             return;
337         }
338
339         boolean shouldGenerate = argsStr.contains("gen") ? true : false;
340         boolean shouldLoad = argsStr.contains("load") ? true : false;
341         boolean shouldPackage = argsStr.contains("package") ? true : false;
342
343         // Config from env variables
344         File dirWorking = new File(System.getenv("GENPROC_WORKING_DIR"));
345         String hostOnboardingAPI = System.getenv("GENPROC_ONBOARDING_API_HOST");
346         String urlToJarIndex = System.getenv("GENPROC_JAR_INDEX_URL");
347
348         String[] paramsToPrint = new String[] {
349             String.format("shouldGenerate=%b", shouldGenerate)
350             , String.format("shouldLoad=%b", shouldLoad)
351             , String.format("Working directory=%s", dirWorking.getName())
352         };
353         LOG.info(String.format("Genprocessor configuration: \n\t%s",
354             String.join("\n\t", paramsToPrint)));
355
356         init(dirWorking);
357
358         // TODO: Need a way to load from file again
359
360         if (shouldGenerate) {
361             CompList compList = OnboardingAPIClient.getComponents(hostOnboardingAPI);
362             LOG.info(String.format("Components retrieved: %d", compList.components.size()));
363
364             for (CompList.CompShort cs : compList.components) {
365                 Comp comp = OnboardingAPIClient.getComponent(cs.getComponentUrlAsURI());
366                 LOG.info(String.format("Component spec: \n\t%s", comp.compSpec.toString("\n\t")));
367
368                 String jarName = Utils.formatNameForJar(comp.compSpec);
369
370                 if (doesJarExist(dirWorking, jarName)) {
371                     LOG.info(String.format("Jar exists: %s.jar", jarName));
372                     continue;
373                 }
374
375                 File dirBuild = new File(dirWorking, jarName);
376
377                 if (dirBuild.exists() || dirBuild.mkdir()) {
378                     generateClassFile(dirBuild.getAbsolutePath(), comp);
379                     writeManifestThing(dirBuild, generateManifestMF(comp.compSpec), "META-INF", "MANIFEST.MF");
380                     writeManifestThing(dirBuild, Arrays.asList(createClassName(comp.compSpec)), "META-INF/services",
381                             "org.apache.nifi.processor.Processor");
382                     copyProcessorClassFile("DCAEProcessor.class", dirBuild);
383                     packageJar(dirWorking, dirBuild, jarName);
384                 }
385             }
386         }
387
388         if (shouldLoad) {
389             List<URL> jarURLs;
390             try {
391                 jarURLs = getDCAEJarsURLs(new URI(urlToJarIndex));
392                 LOG.info(jarURLs.toString());
393             } catch (URISyntaxException e) {
394                 throw new RuntimeException("URL to index.json is bad");
395             }
396
397             for (URL jarURL : jarURLs) {
398                 loadFromJars(new URL[] {jarURL});
399             }
400         }
401     }
402 }
403