PageRenderTime 46ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/notification/src/main/java/com/gooddata/processor/GdcNotification.java

https://github.com/chrbayer84/GoodData-CL
Java | 495 lines | 389 code | 45 blank | 61 comment | 116 complexity | daeba8ec551ad85d63830658980a7b6d MD5 | raw file
Possible License(s): BSD-3-Clause
  1. /*
  2. * Copyright (c) 2009, GoodData Corporation. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without modification, are permitted provided
  5. * that the following conditions are met:
  6. *
  7. * * Redistributions of source code must retain the above copyright notice, this list of conditions and
  8. * the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
  10. * and the following disclaimer in the documentation and/or other materials provided with the distribution.
  11. * * Neither the name of the GoodData Corporation nor the names of its contributors may be used to endorse
  12. * or promote products derived from this software without specific prior written permission.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
  15. * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  16. * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  17. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  18. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  19. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  20. * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  21. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  22. */
  23. package com.gooddata.processor;
  24. import com.gooddata.config.Metric;
  25. import com.gooddata.config.NotificationConfig;
  26. import com.gooddata.config.NotificationMessage;
  27. import com.gooddata.config.Report;
  28. import com.gooddata.exception.*;
  29. import com.gooddata.filter.DuplicateMessageFilter;
  30. import com.gooddata.filter.MessageFilter;
  31. import com.gooddata.integration.rest.GdcRESTApiWrapper;
  32. import com.gooddata.integration.rest.configuration.NamePasswordConfiguration;
  33. import com.gooddata.transport.NotificationTransport;
  34. import com.gooddata.transport.SfdcChatterTransport;
  35. import com.gooddata.util.FileUtil;
  36. import com.sforce.ws.ConnectionException;
  37. import org.apache.commons.cli.*;
  38. import org.apache.commons.jexl2.Expression;
  39. import org.apache.commons.jexl2.JexlContext;
  40. import org.apache.commons.jexl2.JexlEngine;
  41. import org.apache.commons.jexl2.MapContext;
  42. import org.apache.log4j.Logger;
  43. import org.apache.log4j.PropertyConfigurator;
  44. import org.joda.time.DateTime;
  45. import org.joda.time.format.DateTimeFormat;
  46. import org.joda.time.format.DateTimeFormatter;
  47. import java.io.File;
  48. import java.io.FileInputStream;
  49. import java.io.IOException;
  50. import java.io.InputStream;
  51. import java.sql.SQLException;
  52. import java.text.DecimalFormat;
  53. import java.util.List;
  54. import java.util.Properties;
  55. /**
  56. * The GoodData Data Integration CLI processor.
  57. *
  58. * @author jiri.zaloudek
  59. * @author Zdenek Svoboda <zd@gooddata.org>
  60. * @version 1.0
  61. */
  62. public class GdcNotification {
  63. private static Logger l = Logger.getLogger(GdcNotification.class);
  64. //Options data
  65. public static String[] CLI_PARAM_GDC_USERNAME = {"username", "u"};
  66. public static String[] CLI_PARAM_GDC_PASSWORD = {"password", "p"};
  67. public static String[] CLI_PARAM_TRANSPORT_USERNAME = {"transportusername", "d"};
  68. public static String[] CLI_PARAM_TRANSPORT_PASSWORD = {"transportpassword", "c"};
  69. public static String[] CLI_PARAM_GDC_HOST = {"host", "h"};
  70. public static String[] CLI_PARAM_VERSION = {"version", "V"};
  71. public static String CLI_PARAM_CONFIG = "config";
  72. private static String DEFAULT_PROPERTIES = "gdi.properties";
  73. // mandatory options
  74. public static Option[] mandatoryOptions = {};
  75. // optional options
  76. public static Option[] optionalOptions = {
  77. new Option(CLI_PARAM_GDC_USERNAME[1], CLI_PARAM_GDC_USERNAME[0], true, "GoodData username"),
  78. new Option(CLI_PARAM_GDC_PASSWORD[1], CLI_PARAM_GDC_PASSWORD[0], true, "GoodData password"),
  79. new Option(CLI_PARAM_TRANSPORT_USERNAME[1], CLI_PARAM_TRANSPORT_USERNAME[0], true, "Salesforce username"),
  80. new Option(CLI_PARAM_TRANSPORT_PASSWORD[1], CLI_PARAM_TRANSPORT_PASSWORD[0], true, "Salesforce password"),
  81. new Option(CLI_PARAM_GDC_HOST[1], CLI_PARAM_GDC_HOST[0], true, "GoodData host"),
  82. new Option(CLI_PARAM_VERSION[1], CLI_PARAM_VERSION[0], false, "Prints the tool version."),
  83. };
  84. private CliParams cliParams = null;
  85. private boolean finishedSucessfuly = false;
  86. private final static String BUILD_NUMBER = "";
  87. private GdcNotification(CommandLine ln, Properties defaults) {
  88. try {
  89. cliParams = parse(ln, defaults);
  90. cliParams.setHttpConfig(new NamePasswordConfiguration("https",
  91. cliParams.get(CLI_PARAM_GDC_HOST[0]),
  92. cliParams.get(CLI_PARAM_GDC_USERNAME[0]), cliParams.get(CLI_PARAM_GDC_PASSWORD[0])));
  93. String config = cliParams.get(CLI_PARAM_CONFIG);
  94. if (config != null && config.length() > 0) {
  95. execute(config);
  96. } else {
  97. l.error("No config file given.");
  98. commandsHelp();
  99. System.exit(1);
  100. }
  101. finishedSucessfuly = true;
  102. } catch (ConnectionException e) {
  103. l.error("Can't connect to SFDC: " + e.getMessage());
  104. Throwable c = e.getCause();
  105. while (c != null) {
  106. l.error("Caused by: " + c.getMessage());
  107. c = c.getCause();
  108. }
  109. l.debug("Can't connect to SFDC:", e);
  110. } catch (InvalidArgumentException e) {
  111. l.error("Invalid command line argument: " + e.getMessage());
  112. Throwable c = e.getCause();
  113. while (c != null) {
  114. l.error("Caused by: " + c.getMessage());
  115. c = c.getCause();
  116. }
  117. l.debug("Invalid command line argument:", e);
  118. l.info(commandsHelp());
  119. } catch (SfdcException e) {
  120. l.error("Error communicating with SalesForce: " + e.getMessage());
  121. Throwable c = e.getCause();
  122. while (c != null) {
  123. l.error("Caused by: " + c.getMessage());
  124. c = c.getCause();
  125. }
  126. l.debug("Error communicating with SalesForce.", e);
  127. } catch (IOException e) {
  128. l.error("Encountered an IO problem. Please check that all files that you use in your command line arguments and commands exist. More info: '" + e.getMessage() + "'");
  129. Throwable c = e.getCause();
  130. while (c != null) {
  131. l.error("Caused by: " + c.getMessage());
  132. c = c.getCause();
  133. }
  134. l.debug("Encountered an IO problem. Please check that all files that you use in your command line arguments and commands exist. More info: '" + e.getMessage() + "'", e);
  135. } catch (InternalErrorException e) {
  136. Throwable c = e.getCause();
  137. if (c != null && c instanceof SQLException) {
  138. l.error("Error extracting data. Can't process the incoming data. Please check the CSV file " +
  139. "separator and consistency (same number of columns in each row). Also, please make sure " +
  140. "that the number of columns in your XML config file matches the number of rows in your " +
  141. "data source. Make sure that your file is readable by other users (particularly the mysql user). " +
  142. "More info: '" + c.getMessage() + "'");
  143. l.debug("Error extracting data. Can't process the incoming data. Please check the CSV file " +
  144. "separator and consistency (same number of columns in each row). Also, please make sure " +
  145. "that the number of columns in your XML config file matches the number of rows in your " +
  146. "data source. Make sure that your file is readable by other users (particularly the mysql user). " +
  147. "More info: '" + c.getMessage() + "'", c);
  148. } else {
  149. l.error("Internal error: " + e.getMessage());
  150. c = e.getCause();
  151. while (c != null) {
  152. l.error("Caused by: " + c.getMessage());
  153. c = c.getCause();
  154. }
  155. l.debug("REST API invocation error: ", e);
  156. }
  157. } catch (HttpMethodException e) {
  158. l.error("Error executing GoodData REST API: " + e.getMessage());
  159. Throwable c = e.getCause();
  160. while (c != null) {
  161. l.error("Caused by: " + c.getMessage());
  162. c = c.getCause();
  163. }
  164. l.debug("Error executing GoodData REST API.", e);
  165. } catch (GdcRestApiException e) {
  166. l.error("REST API invocation error: " + e.getMessage());
  167. Throwable c = e.getCause();
  168. while (c != null) {
  169. if (c instanceof HttpMethodException) {
  170. HttpMethodException ex = (HttpMethodException) c;
  171. String msg = ex.getMessage();
  172. if (msg != null && msg.length() > 0 && msg.indexOf("/ldm/manage") > 0) {
  173. l.error("Error creating/updating logical data model (executing MAQL DDL).");
  174. if (msg.indexOf(".date") > 0) {
  175. l.error("Bad time dimension schemaReference.");
  176. } else {
  177. l.error("You are either trying to create a data object that already exists " +
  178. "(executing the same MAQL multiple times) or providing a wrong reference " +
  179. "or schemaReference in your XML configuration.");
  180. }
  181. }
  182. }
  183. l.error("Caused by: " + c.getMessage());
  184. c = c.getCause();
  185. }
  186. l.debug("REST API invocation error: ", e);
  187. } catch (GdcException e) {
  188. l.error("Unrecognized error: " + e.getMessage());
  189. Throwable c = e.getCause();
  190. while (c != null) {
  191. l.error("Caused by: " + c.getMessage());
  192. c = c.getCause();
  193. }
  194. l.debug("Unrecognized error: ", e);
  195. }
  196. }
  197. private boolean decide(Object result) {
  198. if (result == null)
  199. return false;
  200. else if (result instanceof Boolean)
  201. return ((Boolean) result).booleanValue();
  202. else if (result instanceof Number)
  203. return ((Number) result).doubleValue() != 0;
  204. else if (result instanceof String)
  205. return ((String) result).length() > 0;
  206. else
  207. return false;
  208. }
  209. private NotificationTransport selectTransport(String uri) {
  210. if (uri.startsWith("sfdc")) {
  211. return SfdcChatterTransport.createTransport(cliParams.get(CLI_PARAM_TRANSPORT_USERNAME[0]),
  212. cliParams.get(CLI_PARAM_TRANSPORT_PASSWORD[0]));
  213. }
  214. throw new InvalidParameterException("Can't find transport for uri " + uri);
  215. }
  216. private final static String DEFAULT_FORMAT = "#,###.00";
  217. private void execute(String config) throws ConnectionException, IOException {
  218. NotificationConfig c = NotificationConfig.fromXml(new File(config));
  219. MessageFilter dupFilter = DuplicateMessageFilter.createFilter();
  220. GdcRESTApiWrapper rest = null;
  221. try {
  222. for (NotificationMessage m : c.getMessages()) {
  223. String dupFilterKind = m.getDupFilterKind();
  224. if (dupFilterKind != null && dupFilterKind.length() > 0) {
  225. if (!dupFilter.filter(m.getMessage(), dupFilterKind)) {
  226. l.debug("Message filtered out by the dup kind filter.");
  227. l.info("Message filtered out by the dup kind filter.");
  228. continue;
  229. }
  230. }
  231. rest = new GdcRESTApiWrapper(cliParams.getHttpConfig());
  232. rest.login();
  233. Expression e = null;
  234. JexlEngine jexl = new JexlEngine();
  235. e = jexl.createExpression(m.getCondition());
  236. JexlContext jc = new MapContext();
  237. List<Metric> metrics = m.getMetrics();
  238. double[] values = null;
  239. if (metrics != null && metrics.size() > 0) {
  240. values = new double[metrics.size()];
  241. for (int i = 0; i < metrics.size(); i++) {
  242. values[i] = rest.computeMetric(metrics.get(i).getUri());
  243. jc.set(metrics.get(i).getAlias(), new Double(values[i]));
  244. }
  245. }
  246. String[] texts = null;
  247. List<Report> reports = m.getReports();
  248. if (reports != null && reports.size() > 0) {
  249. texts = new String[reports.size()];
  250. for (int i = 0; i < reports.size(); i++) {
  251. texts[i] = rest.computeReport(reports.get(i).getUri());
  252. }
  253. }
  254. boolean result = decide(e.evaluate(jc));
  255. if (result) {
  256. NotificationTransport t = selectTransport(m.getUri());
  257. String msg = m.getMessage();
  258. if (values != null && values.length > 0 && metrics != null && metrics.size() > 0) {
  259. for (int i = 0; i < metrics.size(); i++) {
  260. String fmt = metrics.get(i).getFormat();
  261. if (fmt == null || fmt.length() <= 0)
  262. fmt = DEFAULT_FORMAT;
  263. DecimalFormat df = new DecimalFormat(fmt);
  264. msg = msg.replace("%" + metrics.get(i).getAlias() + "%", df.format(values[i]));
  265. }
  266. }
  267. if (texts != null && texts.length > 0 && reports != null && reports.size() > 0) {
  268. for (int i = 0; i < reports.size(); i++) {
  269. msg = msg.replace("%" + reports.get(i).getAlias() + "%", texts[i]);
  270. }
  271. }
  272. String dupFilterExact = m.getDupFilterExact();
  273. if (dupFilterExact != null && dupFilterExact.length() > 0) {
  274. if (!dupFilter.filter(msg, dupFilterExact)) {
  275. l.debug("Message filtered out by the dup exact filter.");
  276. l.info("Message filtered out by the dup exact filter.");
  277. continue;
  278. }
  279. }
  280. String fmt = m.getMessageTimestampFormat();
  281. if (fmt != null && fmt.length() > 0)
  282. t.send(msg + " (at " + getTimestamp(fmt) + ")");
  283. else
  284. t.send(msg);
  285. dupFilter.update(msg);
  286. dupFilter.update(m.getMessage());
  287. l.info("Notification sent.");
  288. }
  289. }
  290. dupFilter.save();
  291. rest.logout();
  292. } catch (Exception e) {
  293. throw new IOException(e);
  294. } finally {
  295. if (rest != null)
  296. rest.logout();
  297. }
  298. }
  299. private String getTimestamp(String fmt) {
  300. DateTimeFormatter f = DateTimeFormat.forPattern(fmt);
  301. return f.print(new DateTime());
  302. }
  303. /**
  304. * Returns all cli options
  305. *
  306. * @return all cli options
  307. */
  308. public static Options getOptions() {
  309. Options ops = new Options();
  310. for (Option o : mandatoryOptions)
  311. ops.addOption(o);
  312. for (Option o : optionalOptions)
  313. ops.addOption(o);
  314. return ops;
  315. }
  316. /**
  317. * Parse and validate the cli arguments
  318. *
  319. * @param ln parsed command line
  320. * @return parsed cli parameters wrapped in the CliParams
  321. * @throws InvalidArgumentException in case of nonexistent or incorrect cli args
  322. */
  323. protected CliParams parse(CommandLine ln, Properties defaults) throws InvalidArgumentException {
  324. l.debug("Parsing cli " + ln);
  325. CliParams cp = new CliParams();
  326. for (Option o : mandatoryOptions) {
  327. String name = o.getLongOpt();
  328. if (ln.hasOption(name))
  329. cp.put(name, ln.getOptionValue(name));
  330. else if (defaults.getProperty(name) != null) {
  331. cp.put(name, defaults.getProperty(name));
  332. } else {
  333. throw new InvalidArgumentException("Missing the '" + name + "' commandline parameter.");
  334. }
  335. }
  336. for (Option o : optionalOptions) {
  337. String name = o.getLongOpt();
  338. if (ln.hasOption(name)) {
  339. cp.put(name, ln.getOptionValue(name));
  340. } else if (defaults.getProperty(name) != null) {
  341. cp.put(name, defaults.getProperty(name));
  342. }
  343. }
  344. if (cp.containsKey(CLI_PARAM_VERSION[0])) {
  345. l.info("GoodData Notification Tool version 1.2.67" +
  346. ((BUILD_NUMBER.length() > 0) ? ", build " + BUILD_NUMBER : "."));
  347. System.exit(0);
  348. }
  349. // use default host if there is no host in the CLI params
  350. if (!cp.containsKey(CLI_PARAM_GDC_HOST[0])) {
  351. cp.put(CLI_PARAM_GDC_HOST[0], Defaults.DEFAULT_HOST);
  352. }
  353. l.debug("Using host " + cp.get(CLI_PARAM_GDC_HOST[0]));
  354. if (ln.getArgs().length == 0) {
  355. throw new InvalidArgumentException("No config file has been given, quitting.");
  356. }
  357. String configs = "";
  358. for (final String arg : ln.getArgs()) {
  359. if (configs.length() > 0)
  360. configs += "," + arg;
  361. else
  362. configs += arg;
  363. }
  364. cp.put(CLI_PARAM_CONFIG, configs);
  365. return cp;
  366. }
  367. /**
  368. * Returns the help for commands
  369. *
  370. * @return help text
  371. */
  372. public static String commandsHelp() {
  373. try {
  374. final InputStream is = CliParams.class.getResourceAsStream("/com/gooddata/processor/COMMANDS.txt");
  375. if (is == null)
  376. throw new IOException();
  377. return FileUtil.readStringFromStream(is);
  378. } catch (IOException e) {
  379. l.error("Could not read com/gooddata/processor/COMMANDS.txt");
  380. }
  381. return "";
  382. }
  383. private static boolean checkJavaVersion() {
  384. String version = System.getProperty("java.version");
  385. if (version.startsWith("1.8") || version.startsWith("1.7") || version.startsWith("1.6") || version.startsWith("1.5"))
  386. return true;
  387. l.error("You're running Java " + version + ". Please use Java 1.5 or higher for running this tool. " +
  388. "Please refer to http://java.sun.com/javase/downloads/index.jsp for a more recent Java version.");
  389. throw new InternalErrorException("You're running Java " + version + ". Please use use Java 1.5 or higher for running this tool. " +
  390. "Please refer to http://java.sun.com/javase/downloads/index.jsp for a more recent Java version.");
  391. }
  392. /**
  393. * The main CLI processor
  394. *
  395. * @param args command line argument
  396. */
  397. public static void main(String[] args) {
  398. checkJavaVersion();
  399. String logConfig = System.getProperty("log4j.configuration");
  400. if (logConfig != null && logConfig.length() > 0) {
  401. File lc = new File(logConfig);
  402. if (lc.exists()) {
  403. PropertyConfigurator.configure(logConfig);
  404. Properties defaults = loadDefaults();
  405. try {
  406. Options o = getOptions();
  407. CommandLineParser parser = new GnuParser();
  408. CommandLine cmdline = parser.parse(o, args);
  409. GdcNotification gdi = new GdcNotification(cmdline, defaults);
  410. if (!gdi.finishedSucessfuly) {
  411. System.exit(1);
  412. }
  413. } catch (org.apache.commons.cli.ParseException e) {
  414. l.error("Error parsing command line parameters: " + e.getMessage());
  415. l.debug("Error parsing command line parameters", e);
  416. }
  417. } else {
  418. l.error("Can't find the logging config. Please configure the logging via the log4j.configuration.");
  419. }
  420. } else {
  421. l.error("Can't find the logging config. Please configure the logging via the log4j.configuration.");
  422. }
  423. }
  424. /**
  425. * Loads default values of common parameters from a properties file searching
  426. * the working directory and user's home.
  427. *
  428. * @return default configuration
  429. */
  430. private static Properties loadDefaults() {
  431. final String[] dirs = new String[]{"user.dir", "user.home"};
  432. final Properties props = new Properties();
  433. for (final String d : dirs) {
  434. String path = System.getProperty(d) + File.separator + DEFAULT_PROPERTIES;
  435. File f = new File(path);
  436. if (f.exists() && f.canRead()) {
  437. try {
  438. FileInputStream is = new FileInputStream(f);
  439. props.load(is);
  440. l.debug("Successfully red the gdi configuration from '" + f.getAbsolutePath() + "'.");
  441. return props;
  442. } catch (IOException e) {
  443. l.warn("Readable gdi configuration '" + f.getAbsolutePath() + "' found be error occurred reading it.");
  444. l.debug("Error reading gdi configuration '" + f.getAbsolutePath() + "': ", e);
  445. }
  446. }
  447. }
  448. return props;
  449. }
  450. }