PageRenderTime 64ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://github.com/lkundrak/GoodData-CL
Java | 505 lines | 403 code | 46 blank | 56 comment | 99 complexity | ffc39329446f1250223e712423e05cab MD5 | raw file
  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 java.io.*;
  25. import java.sql.SQLException;
  26. import java.text.DecimalFormat;
  27. import java.util.*;
  28. import com.gooddata.config.Metric;
  29. import com.gooddata.config.NotificationConfig;
  30. import com.gooddata.config.NotificationMessage;
  31. import com.gooddata.config.Report;
  32. import com.gooddata.exception.*;
  33. import com.gooddata.filter.DuplicateMessageFilter;
  34. import com.gooddata.filter.MessageFilter;
  35. import com.gooddata.integration.rest.GdcRESTApiWrapper;
  36. import com.gooddata.transport.NotificationTransport;
  37. import com.gooddata.transport.SfdcChatterTransport;
  38. import com.sforce.ws.ConnectionException;
  39. import org.apache.commons.cli.CommandLine;
  40. import org.apache.commons.cli.CommandLineParser;
  41. import org.apache.commons.cli.GnuParser;
  42. import org.apache.commons.cli.Option;
  43. import org.apache.commons.cli.Options;
  44. import org.apache.commons.jexl2.Expression;
  45. import org.apache.commons.jexl2.JexlContext;
  46. import org.apache.commons.jexl2.JexlEngine;
  47. import org.apache.commons.jexl2.MapContext;
  48. import org.apache.log4j.Logger;
  49. import org.apache.log4j.PropertyConfigurator;
  50. import com.gooddata.integration.rest.configuration.NamePasswordConfiguration;
  51. import com.gooddata.util.FileUtil;
  52. import org.joda.time.DateTime;
  53. import org.joda.time.format.DateTimeFormat;
  54. import org.joda.time.format.DateTimeFormatter;
  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. }
  97. else {
  98. l.error("No config file given.");
  99. commandsHelp();
  100. System.exit(1);
  101. }
  102. finishedSucessfuly = true;
  103. }
  104. catch (ConnectionException e) {
  105. l.error("Can't connect to SFDC: "+e.getMessage());
  106. Throwable c = e.getCause();
  107. while(c!=null) {
  108. l.error("Caused by: "+c.getMessage());
  109. c = c.getCause();
  110. }
  111. l.debug("Can't connect to SFDC:",e);
  112. }
  113. catch (InvalidArgumentException e) {
  114. l.error("Invalid command line argument: "+e.getMessage());
  115. Throwable c = e.getCause();
  116. while(c!=null) {
  117. l.error("Caused by: "+c.getMessage());
  118. c = c.getCause();
  119. }
  120. l.debug("Invalid command line argument:",e);
  121. l.info(commandsHelp());
  122. }
  123. catch (SfdcException e) {
  124. l.error("Error communicating with SalesForce: "+e.getMessage());
  125. Throwable c = e.getCause();
  126. while(c!=null) {
  127. l.error("Caused by: "+c.getMessage());
  128. c = c.getCause();
  129. }
  130. l.debug("Error communicating with SalesForce.",e);
  131. }
  132. catch (IOException e) {
  133. 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()+"'");
  134. Throwable c = e.getCause();
  135. while(c!=null) {
  136. l.error("Caused by: "+c.getMessage());
  137. c = c.getCause();
  138. }
  139. 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);
  140. }
  141. catch (InternalErrorException e) {
  142. Throwable c = e.getCause();
  143. if( c != null && c instanceof SQLException) {
  144. l.error("Error extracting data. Can't process the incoming data. Please check the CSV file " +
  145. "separator and consistency (same number of columns in each row). Also, please make sure " +
  146. "that the number of columns in your XML config file matches the number of rows in your " +
  147. "data source. Make sure that your file is readable by other users (particularly the mysql user). " +
  148. "More info: '"+c.getMessage()+"'");
  149. l.debug("Error extracting data. Can't process the incoming data. Please check the CSV file " +
  150. "separator and consistency (same number of columns in each row). Also, please make sure " +
  151. "that the number of columns in your XML config file matches the number of rows in your " +
  152. "data source. Make sure that your file is readable by other users (particularly the mysql user). " +
  153. "More info: '"+c.getMessage()+"'",c);
  154. }
  155. else {
  156. l.error("Internal error: "+e.getMessage());
  157. c = e.getCause();
  158. while(c!=null) {
  159. l.error("Caused by: "+c.getMessage());
  160. c = c.getCause();
  161. }
  162. l.debug("REST API invocation error: ",e);
  163. }
  164. }
  165. catch (HttpMethodException e) {
  166. l.error("Error executing GoodData REST API: "+e.getMessage());
  167. Throwable c = e.getCause();
  168. while(c!=null) {
  169. l.error("Caused by: "+c.getMessage());
  170. c = c.getCause();
  171. }
  172. l.debug("Error executing GoodData REST API.",e);
  173. }
  174. catch (GdcRestApiException e) {
  175. l.error("REST API invocation error: "+e.getMessage());
  176. Throwable c = e.getCause();
  177. while(c!=null) {
  178. if(c instanceof HttpMethodException) {
  179. HttpMethodException ex = (HttpMethodException)c;
  180. String msg = ex.getMessage();
  181. if(msg != null && msg.length()>0 && msg.indexOf("/ldm/manage")>0) {
  182. l.error("Error creating/updating logical data model (executing MAQL DDL).");
  183. if(msg.indexOf(".date")>0) {
  184. l.error("Bad time dimension schemaReference.");
  185. }
  186. else {
  187. l.error("You are either trying to create a data object that already exists " +
  188. "(executing the same MAQL multiple times) or providing a wrong reference " +
  189. "or schemaReference in your XML configuration.");
  190. }
  191. }
  192. }
  193. l.error("Caused by: "+c.getMessage());
  194. c = c.getCause();
  195. }
  196. l.debug("REST API invocation error: ", e);
  197. }
  198. catch (GdcException e) {
  199. l.error("Unrecognized error: "+e.getMessage());
  200. Throwable c = e.getCause();
  201. while(c!=null) {
  202. l.error("Caused by: "+c.getMessage());
  203. c = c.getCause();
  204. }
  205. l.debug("Unrecognized error: ",e);
  206. }
  207. }
  208. private boolean decide(Object result) {
  209. if(result == null)
  210. return false;
  211. else if(result instanceof Boolean)
  212. return ((Boolean)result).booleanValue();
  213. else if (result instanceof Number)
  214. return ((Number)result).doubleValue() != 0;
  215. else if(result instanceof String)
  216. return ((String)result).length() > 0;
  217. else
  218. return false;
  219. }
  220. private NotificationTransport selectTransport(String uri) {
  221. if(uri.startsWith("sfdc")) {
  222. return SfdcChatterTransport.createTransport(cliParams.get(CLI_PARAM_TRANSPORT_USERNAME[0]),
  223. cliParams.get(CLI_PARAM_TRANSPORT_PASSWORD[0]));
  224. }
  225. throw new InvalidParameterException("Can't find transport for uri "+uri);
  226. }
  227. private final static String DEFAULT_FORMAT = "#,###.00";
  228. private void execute(String config) throws ConnectionException, IOException {
  229. NotificationConfig c = NotificationConfig.fromXml(new File(config));
  230. MessageFilter dupFilter = DuplicateMessageFilter.createFilter();
  231. GdcRESTApiWrapper rest = null;
  232. try {
  233. for(NotificationMessage m : c.getMessages()) {
  234. String dupFilterKind = m.getDupFilterKind();
  235. if(dupFilterKind != null && dupFilterKind.length()>0) {
  236. if(!dupFilter.filter(m.getMessage(), dupFilterKind)) {
  237. l.debug("Message filtered out by the dup kind filter.");
  238. l.info("Message filtered out by the dup kind filter.");
  239. continue;
  240. }
  241. }
  242. rest = new GdcRESTApiWrapper(cliParams.getHttpConfig());
  243. rest.login();
  244. Expression e = null;
  245. JexlEngine jexl = new JexlEngine();
  246. e = jexl.createExpression(m.getCondition());
  247. JexlContext jc = new MapContext();
  248. List<Metric> metrics = m.getMetrics();
  249. double[] values = null;
  250. if(metrics != null && metrics.size() >0) {
  251. values = new double[metrics.size()];
  252. for(int i=0; i<metrics.size(); i++) {
  253. values[i] = rest.computeMetric(metrics.get(i).getUri());
  254. jc.set(metrics.get(i).getAlias(), new Double(values[i]));
  255. }
  256. }
  257. String[] texts = null;
  258. List<Report> reports = m.getReports();
  259. if(reports != null && reports.size() >0) {
  260. texts = new String[reports.size()];
  261. for(int i=0; i<reports.size(); i++) {
  262. texts[i] = rest.computeReport(reports.get(i).getUri());
  263. }
  264. }
  265. boolean result = decide(e.evaluate(jc));
  266. if(result) {
  267. NotificationTransport t = selectTransport(m.getUri());
  268. String msg = m.getMessage();
  269. if(values != null && values.length > 0 && metrics != null && metrics.size() > 0) {
  270. for(int i = 0 ; i < metrics.size(); i++) {
  271. String fmt = metrics.get(i).getFormat();
  272. if(fmt == null || fmt.length() <= 0)
  273. fmt = DEFAULT_FORMAT;
  274. DecimalFormat df = new DecimalFormat(fmt);
  275. msg = msg.replace("%"+metrics.get(i).getAlias()+"%", df.format(values[i]));
  276. }
  277. }
  278. if(texts!= null && texts.length > 0 && reports != null && reports.size() > 0) {
  279. for(int i = 0 ; i < reports.size(); i++) {
  280. msg = msg.replace("%"+reports.get(i).getAlias()+"%", texts[i]);
  281. }
  282. }
  283. String dupFilterExact = m.getDupFilterExact();
  284. if(dupFilterExact != null && dupFilterExact.length()>0) {
  285. if(!dupFilter.filter(msg, dupFilterExact)) {
  286. l.debug("Message filtered out by the dup exact filter.");
  287. l.info("Message filtered out by the dup exact filter.");
  288. continue;
  289. }
  290. }
  291. String fmt = m.getMessageTimestampFormat();
  292. if(fmt != null && fmt.length() > 0)
  293. t.send(msg+" (at "+getTimestamp(fmt)+")");
  294. else
  295. t.send(msg);
  296. dupFilter.update(msg);
  297. dupFilter.update(m.getMessage());
  298. l.info("Notification sent.");
  299. }
  300. }
  301. dupFilter.save();
  302. rest.logout();
  303. }
  304. catch (Exception e) {
  305. throw new IOException(e);
  306. } finally {
  307. if (rest != null)
  308. rest.logout();
  309. }
  310. }
  311. private String getTimestamp(String fmt) {
  312. DateTimeFormatter f = DateTimeFormat.forPattern(fmt);
  313. return f.print(new DateTime());
  314. }
  315. /**
  316. * Returns all cli options
  317. * @return all cli options
  318. */
  319. public static Options getOptions() {
  320. Options ops = new Options();
  321. for( Option o : mandatoryOptions)
  322. ops.addOption(o);
  323. for( Option o : optionalOptions)
  324. ops.addOption(o);
  325. return ops;
  326. }
  327. /**
  328. * Parse and validate the cli arguments
  329. * @param ln parsed command line
  330. * @return parsed cli parameters wrapped in the CliParams
  331. * @throws InvalidArgumentException in case of nonexistent or incorrect cli args
  332. */
  333. protected CliParams parse(CommandLine ln, Properties defaults) throws InvalidArgumentException {
  334. l.debug("Parsing cli "+ln);
  335. CliParams cp = new CliParams();
  336. for( Option o : mandatoryOptions) {
  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. } else {
  343. throw new InvalidArgumentException("Missing the '"+name+"' commandline parameter.");
  344. }
  345. }
  346. for( Option o : optionalOptions) {
  347. String name = o.getLongOpt();
  348. if (ln.hasOption(name)) {
  349. cp.put(name,ln.getOptionValue(name));
  350. } else if (defaults.getProperty(name) != null) {
  351. cp.put(name, defaults.getProperty(name));
  352. }
  353. }
  354. if(cp.containsKey(CLI_PARAM_VERSION[0])) {
  355. l.info("GoodData Notification Tool version 1.2.45" +
  356. ((BUILD_NUMBER.length()>0) ? ", build "+BUILD_NUMBER : "."));
  357. System.exit(0);
  358. }
  359. // use default host if there is no host in the CLI params
  360. if(!cp.containsKey(CLI_PARAM_GDC_HOST[0])) {
  361. cp.put(CLI_PARAM_GDC_HOST[0], Defaults.DEFAULT_HOST);
  362. }
  363. l.debug("Using host "+cp.get(CLI_PARAM_GDC_HOST[0]));
  364. if (ln.getArgs().length == 0) {
  365. throw new InvalidArgumentException("No config file has been given, quitting.");
  366. }
  367. String configs = "";
  368. for (final String arg : ln.getArgs()) {
  369. if(configs.length()>0)
  370. configs += ","+arg;
  371. else
  372. configs += arg;
  373. }
  374. cp.put(CLI_PARAM_CONFIG, configs);
  375. return cp;
  376. }
  377. /**
  378. * Returns the help for commands
  379. * @return help text
  380. */
  381. public static String commandsHelp() {
  382. try {
  383. final InputStream is = CliParams.class.getResourceAsStream("/com/gooddata/processor/COMMANDS.txt");
  384. if (is == null)
  385. throw new IOException();
  386. return FileUtil.readStringFromStream(is);
  387. } catch (IOException e) {
  388. l.error("Could not read com/gooddata/processor/COMMANDS.txt");
  389. }
  390. return "";
  391. }
  392. private static boolean checkJavaVersion() {
  393. String version = System.getProperty("java.version");
  394. if(version.startsWith("1.8") || version.startsWith("1.7") || version.startsWith("1.6") || version.startsWith("1.5"))
  395. return true;
  396. l.error("You're running Java "+version+". Please use Java 1.5 or higher for running this tool. " +
  397. "Please refer to http://java.sun.com/javase/downloads/index.jsp for a more recent Java version.");
  398. throw new InternalErrorException("You're running Java "+version+". Please use use Java 1.5 or higher for running this tool. " +
  399. "Please refer to http://java.sun.com/javase/downloads/index.jsp for a more recent Java version.");
  400. }
  401. /**
  402. * The main CLI processor
  403. * @param args command line argument
  404. */
  405. public static void main(String[] args) {
  406. checkJavaVersion();
  407. String logConfig = System.getProperty("log4j.configuration");
  408. if(logConfig != null && logConfig.length()>0) {
  409. File lc = new File(logConfig);
  410. if(lc.exists()) {
  411. PropertyConfigurator.configure(logConfig);
  412. Properties defaults = loadDefaults();
  413. try {
  414. Options o = getOptions();
  415. CommandLineParser parser = new GnuParser();
  416. CommandLine cmdline = parser.parse(o, args);
  417. GdcNotification gdi = new GdcNotification(cmdline, defaults);
  418. if (!gdi.finishedSucessfuly) {
  419. System.exit(1);
  420. }
  421. } catch (org.apache.commons.cli.ParseException e) {
  422. l.error("Error parsing command line parameters: "+e.getMessage());
  423. l.debug("Error parsing command line parameters",e);
  424. }
  425. }
  426. else {
  427. l.error("Can't find the logging config. Please configure the logging via the log4j.configuration.");
  428. }
  429. }
  430. else {
  431. l.error("Can't find the logging config. Please configure the logging via the log4j.configuration.");
  432. }
  433. }
  434. /**
  435. * Loads default values of common parameters from a properties file searching
  436. * the working directory and user's home.
  437. * @return default configuration
  438. */
  439. private static Properties loadDefaults() {
  440. final String[] dirs = new String[]{ "user.dir", "user.home" };
  441. final Properties props = new Properties();
  442. for (final String d : dirs) {
  443. String path = System.getProperty(d) + File.separator + DEFAULT_PROPERTIES;
  444. File f = new File(path);
  445. if (f.exists() && f.canRead()) {
  446. try {
  447. FileInputStream is = new FileInputStream(f);
  448. props.load(is);
  449. l.debug("Successfully red the gdi configuration from '" + f.getAbsolutePath() + "'.");
  450. return props;
  451. } catch (IOException e) {
  452. l.warn("Readable gdi configuration '" + f.getAbsolutePath() + "' found be error occurred reading it.");
  453. l.debug("Error reading gdi configuration '" + f.getAbsolutePath() + "': ", e);
  454. }
  455. }
  456. }
  457. return props;
  458. }
  459. }