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