Add a MassMail Batch Program
[aaf/authz.git] / auth / auth-batch / src / main / java / org / onap / aaf / auth / batch / reports / Notify.java
1 /**
2  * ============LICENSE_START====================================================
3  * org.onap.aaf
4  * ===========================================================================
5  * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6  * ===========================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END====================================================
19  *
20  */package org.onap.aaf.auth.batch.reports;
21
22  import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.InvocationTargetException;
28 import java.text.ParseException;
29 import java.util.ArrayList;
30 import java.util.Date;
31 import java.util.GregorianCalendar;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.Set;
37 import java.util.TreeMap;
38
39 import org.onap.aaf.auth.batch.Batch;
40 import org.onap.aaf.auth.batch.approvalsets.Pending;
41 import org.onap.aaf.auth.batch.helpers.CQLBatch;
42 import org.onap.aaf.auth.batch.helpers.CQLBatchLoop;
43 import org.onap.aaf.auth.batch.helpers.LastNotified;
44 import org.onap.aaf.auth.batch.reports.bodies.NotifyBody;
45 import org.onap.aaf.auth.batch.reports.bodies.NotifyPendingApprBody;
46 import org.onap.aaf.auth.env.AuthzTrans;
47 import org.onap.aaf.auth.org.Mailer;
48 import org.onap.aaf.auth.org.Organization.Identity;
49 import org.onap.aaf.auth.org.OrganizationException;
50 import org.onap.aaf.cadi.Access;
51 import org.onap.aaf.cadi.CadiException;
52 import org.onap.aaf.cadi.PropAccess;
53 import org.onap.aaf.cadi.util.Holder;
54 import org.onap.aaf.cadi.util.CSV;
55 import org.onap.aaf.misc.env.APIException;
56 import org.onap.aaf.misc.env.TimeTaken;
57 import org.onap.aaf.misc.env.Trans;
58 import org.onap.aaf.misc.env.util.Chrono;
59
60  public class Notify extends Batch {
61      private static final String HTML_CSS = "HTML_CSS";
62      private final Mailer mailer;
63      private final String header;
64      private final String footer;
65      private final int maxEmails;
66      private final int indent;
67      private final boolean urgent;
68      public final String guiURL;
69      private PropAccess access;
70      private AuthzTrans noAvg;
71      private CQLBatch cqlBatch;
72     private LastNotified  lastN;
73     private CQLBatchLoop cbl;
74
75      public Notify(AuthzTrans trans) throws APIException, IOException, OrganizationException {
76          super(trans.env());
77          access = env.access();
78          session = cluster.connect();
79
80          String mailerCls = env.getProperty("MAILER");
81          String mailFrom = env.getProperty("MAIL_FROM");
82          String header_html = env.getProperty("HEADER_HTML");
83          String footer_html = env.getProperty("FOOTER_HTML");
84          String str = env.getProperty("MAX_EMAIL");
85          guiURL = env.getProperty("GUI_URL");
86          maxEmails = str==null||str.isEmpty()?Integer.MAX_VALUE:Integer.parseInt(str);
87          if(mailerCls==null || mailFrom==null || guiURL==null || header_html==null || footer_html==null) {
88              throw new APIException("Notify requires MAILER, MAILER_FROM, GUI_URL, HEADER_HTML and FOOTER_HTML properties");
89          }
90          try {
91              Class<?> mailc = Class.forName(mailerCls);
92              Constructor<?> mailcst = mailc.getConstructor(Access.class);
93              mailer = (Mailer)mailcst.newInstance(env.access());
94          } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
95              throw new APIException("Unable to construct " + mailerCls,e);
96          }
97
98          String line;
99          StringBuilder sb = new StringBuilder();
100          File fhh = new File(header_html);
101          if(!fhh.exists()) {
102              throw new APIException(header_html + " does not exist");
103          }
104          BufferedReader br = new BufferedReader(new FileReader(fhh));
105          try {
106              while((line=br.readLine())!=null) {
107                  sb.append(line);
108                  sb.append('\n');
109              }
110              String html_css = env.getProperty(HTML_CSS);
111              String temp;
112              int hc = sb.indexOf(HTML_CSS);
113              if(hc!=0 && html_css!=null) {
114                  temp = sb.replace(hc,hc+HTML_CSS.length(), html_css).toString();
115              } else {
116                  temp = sb.toString();
117              }
118              header = temp.replace("AAF:ENV", batchEnv);
119          } finally {
120              br.close();
121          }
122
123          // Establish index from header
124          int lastTag = header.lastIndexOf('<');
125          if(lastTag>0) {
126              int prevCR = header.lastIndexOf('\n',lastTag);
127              if(prevCR>0) {
128                  indent = lastTag-prevCR;
129              } else {
130                  indent = 6; //arbitrary
131              }
132          } else {
133              indent = 6;
134          }
135
136          urgent = false;
137
138          sb.setLength(0);
139          fhh = new File(footer_html);
140          if(!fhh.exists()) {
141              throw new APIException(footer_html + " does not exist");
142          }
143
144          br = new BufferedReader(new FileReader(fhh));
145          try {
146              while((line=br.readLine())!=null) {
147                  sb.append(line);
148                  sb.append('\n');
149              }
150              footer = sb.toString();
151          } finally {
152              br.close();
153          }
154
155          noAvg = trans.env().newTransNoAvg();
156          cqlBatch = new CQLBatch(noAvg.debug(),session);
157           cbl = new CQLBatchLoop(cqlBatch,50,dryRun);
158
159           lastN = new LastNotified(session);
160      }
161
162      /*
163       * Note: We try to put things related to Notify as Main Class in Run, where we might have put in
164       * Constructor, so that we can have other Classes call just the "notify" method.
165       */
166      @Override
167      protected void run(AuthzTrans trans) {
168
169          final Holder<List<String>> info = new Holder<>(null);
170          final Set<String> errorSet = new HashSet<>();
171          String fmt = "%s"+Chrono.dateOnlyStamp()+".csv";
172
173          try {
174              // Class Load possible data
175              NotifyBody.load(env.access());
176
177
178              // Create Intermediate Output
179              File logDir = logDir();
180              Set<File> notifyFile = new HashSet<>();
181              if(args().length>0) {
182                  for(int i=0;i<args().length;++i) {
183                      notifyFile.add(new File(logDir, args()[i]));
184                  }
185              } else {
186                  File file;
187                  for(NotifyBody nb : NotifyBody.getAll()) {
188                      file = new File(logDir,String.format(fmt, nb.name()));
189                      if(file.exists()) {
190                          trans.info().printf("Processing '%s' in %s",nb.type(),file.getCanonicalPath());
191                          notifyFile.add(file);
192                      } else {
193                          trans.info().printf("No Files found for %s",nb.name());
194                      }
195                  }
196              }
197
198              for(File f : notifyFile) {
199                  CSV csv = new CSV(env.access(),f);
200                  try {
201                      csv.visit(new CSV.Visitor() {
202                          @Override
203                          public void visit(List<String> row) throws IOException, CadiException {
204                              if("info".equals(row.get(0))) {
205                                  info.set(row);
206                              }
207                              if(info.get()==null) {
208                                  throw new CadiException("First line of Feed MUST contain 'info' record");
209                              }                             String key = row.get(0)+'|'+info.get().get(1);
210                              NotifyBody body = NotifyBody.get(key);
211                              if(body==null) {
212                                  errorSet.add("No NotifyBody defined for " + key);
213                              } else {
214                                  body.store(row);
215                              }
216                          }
217                      });
218                  } catch (IOException | CadiException e) {
219                      e.printStackTrace();
220                  }
221
222              }
223
224              // now create Notification
225              for(NotifyBody nb : NotifyBody.getAll()) {
226                  int count = notify(noAvg, nb);
227                  trans.info().printf("Emailed %d for %s",count,nb.name());
228              }
229
230              //
231              // Do Pending Approval Notifies. We do this separately, because we are consolidating
232              // all the new entries, etc.
233              //
234              List<CSV> csvList = new ArrayList<>();
235              for(String s : new String[] {"Approvals","ApprovalsNew"}) {
236                  File f = new File(logDir(),String.format(fmt, s));
237                  if(f.exists()) {
238                      csvList.add(new CSV(access,f));
239                  }
240              }
241
242              Map<String,Pending> mpending = new TreeMap<>();
243              Holder<Integer> count = new Holder<>(0);
244              for(CSV approveCSV : csvList) {
245                 TimeTaken tt = trans.start("Load Analyzed Reminders",Trans.SUB,approveCSV.name());
246                 try {
247                     approveCSV.visit(row -> {
248                         switch(row.get(0)) {
249 //                            case "info":
250 //                                break;
251                             case Pending.REMIND:
252                                 try {
253                                     String user = row.get(1);
254                                     Pending p = new Pending(row);
255                                     Pending mp = mpending.get(user);
256                                     if(mp==null) {
257                                         mpending.put(user, p);
258                                     } else {
259                                         mp.inc(p); // FYI, unlikely
260                                     }
261                                     count.set(count.get()+1);
262                                 } catch (ParseException e) {
263                                     trans.error().log(e);
264                                 }
265                             break;
266                         }
267                     });
268                 } catch (IOException | CadiException e) {
269                     trans.error().log(e);
270                 } finally {
271                     tt.done();
272                 }
273             }
274             trans.info().printf("Read %d Reminder Rows", count.get());
275
276             NotifyPendingApprBody npab = new NotifyPendingApprBody(access);
277
278             GregorianCalendar gc = new GregorianCalendar();
279             gc.add(GregorianCalendar.DAY_OF_MONTH, 7); // Get from INFO?
280             Date oneWeek = gc.getTime();
281             CSV.Saver rs = new CSV.Saver();
282
283             TimeTaken tt = trans.start("Obtain Last Notifications for Approvers", Trans.SUB);
284             try {
285                 lastN.add(mpending.keySet());
286             } finally {
287                 tt.done();
288             }
289
290             Pending p;
291
292             tt = trans.start("Notify for Pending", Trans.SUB);
293             List<String> idList = new ArrayList<>();
294             String id;
295             try {
296                 for(Entry<String, Pending> es : mpending.entrySet()) {
297                     id = es.getKey();
298                     idList.clear();
299                     idList.add(id);
300                     p = es.getValue();
301                     boolean nap = p.newApprovals();
302                     if(!nap) {
303                         Date dateLastNotified = lastN.lastNotified(id,"pending","");
304                         if(dateLastNotified==null || dateLastNotified.after(oneWeek) ) {
305                             nap=true;
306                         }
307                     }
308                     if(nap) {
309                         rs.row("appr", id,p.qty(),batchEnv);
310                         npab.store(rs.asList());
311                         if(notify(noAvg, npab)>0) {
312                             npab.record(trans,cbl.inc(), id, idList, lastN);
313                             npab.inc();
314                         }
315                     }
316                 }
317             } finally {
318                 cbl.flush();
319                 tt.done();
320                 trans.info().printf("Notified %d persons of Pending Approvals", npab.count());
321             }
322
323         } catch (APIException | IOException e1) {
324             trans.error().log(e1);
325         } finally {
326              for(String s : errorSet) {
327                  trans.audit().log(s);
328              }
329          }
330      }
331
332      private int notify(AuthzTrans trans, NotifyBody nb) {
333          List<String> toList = new ArrayList<>();
334          List<String> ccList = new ArrayList<>();
335          List<String> idList = new ArrayList<>();
336
337          String run = nb.type()+nb.name();
338          String test = dryRun?run:null;
339
340          ONE_EMAIL:
341          for(String id : nb.users()) {
342              toList.clear();
343              ccList.clear();
344              idList.clear();
345              try {
346                  List<Identity> identities = trans.org().getIDs(trans, id, nb.escalation());
347                  if(identities.isEmpty()) {
348                      trans.warn().printf("%s is invalid for this Organization. Skipping notification.",id);
349                  } else {
350                      Identity identity = null;
351                      for(Identity ident : identities) {
352                          if(identity==null) {
353                              identity = ident;
354                              toList.add(ident.email());
355                          } else {
356                              ccList.add(ident.email());
357                          }
358                          idList.add(ident.fullID());
359                      }
360                      if(identity==null) { // Actually, identity can't be null here, because
361                          break;           // if(identities.isEmpty() {..} else {... <here>
362                      }                    // So this is here to avoid Sonar false positive only
363                      StringBuilder content = new StringBuilder();
364                      content.append(String.format(header,version,Identity.mixedCase(identity.firstName())));
365
366                      nb.body(trans, content, indent, this, id);
367                      content.append(footer);
368
369                      if(mailer.sendEmail(trans, test, toList, ccList, nb.subject(),content.toString(), urgent)) {
370                         nb.record(trans,cbl.inc(), id, idList, lastN);
371                         nb.inc();
372                      } else {
373                          trans.error().log("Mailer failed to send Mail");
374                      }
375                      if(maxEmails>0 && nb.count()>=maxEmails) {
376                          break ONE_EMAIL;
377                      }
378                  }
379              } catch (OrganizationException e) {
380                  trans.error().log(e);
381              }
382          }
383          cbl.flush();
384          return nb.count();
385      }
386
387     /* (non-Javadoc)
388      * @see org.onap.aaf.auth.batch.Batch#_close(org.onap.aaf.auth.env.AuthzTrans)
389      */
390     @Override
391     protected void _close(AuthzTrans trans) {
392         cbl.flush();
393     }
394
395  }