Imported from dev1.link2tek.net CommEngine.git
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

662 行
22 KiB

  1. package altk.comm.engine;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.FileNotFoundException;
  5. import java.io.IOException;
  6. import java.io.PrintWriter;
  7. import java.security.KeyManagementException;
  8. import java.security.KeyStoreException;
  9. import java.security.NoSuchAlgorithmException;
  10. import java.util.Enumeration;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. import java.util.Properties;
  14. import java.util.Vector;
  15. import java.util.concurrent.Executors;
  16. import java.util.concurrent.ScheduledExecutorService;
  17. import java.util.concurrent.TimeUnit;
  18. import javax.servlet.ServletContext;
  19. import javax.servlet.http.HttpServlet;
  20. import javax.servlet.http.HttpServletRequest;
  21. import javax.servlet.http.HttpServletResponse;
  22. import org.apache.log4j.Logger;
  23. import org.apache.log4j.PropertyConfigurator;
  24. import altk.comm.engine.Broadcast.BroadcastState;
  25. import altk.comm.engine.exception.BroadcastError;
  26. import altk.comm.engine.exception.BroadcastException;
  27. import altk.comm.engine.exception.PlatformError;
  28. import altk.comm.engine.exception.PlatformException;
  29. import altk.comm.engine.postback.PostBack;
  30. @SuppressWarnings("serial")
  31. public abstract class CommEngine extends HttpServlet
  32. {
  33. static final String REQUEST_TOP_ELEMENT_NAME_DEFAULT = "Request";
  34. private static final int SCHEDULER_THREAD_POOL_SIZE = 1;
  35. private static final long DEAD_BROADCAST_VIEWING_PERIOD_DEFAULT = 60;
  36. private static final int SERVICE_THREADPOOL_SIZE_DEFAULT = 1;
  37. private static final int POSTBACK_THREADPOOL_SIZE_DEFAULT = 2;
  38. private static final int POSTBACK_MAX_QUEUE_SIZE_DEFAULT = 10000;
  39. private static final int POSTBACK_MAX_BATCH_SIZE_DEFAULT = 100;
  40. /**
  41. * Maps a broadcastId to a broadcast.
  42. */
  43. private Map<String, Broadcast> broadcasts;
  44. protected boolean notInService;
  45. protected Properties config;
  46. protected Map<String, PostBack> postBackMap;
  47. protected final String engineName; // e.g. "broadcast_sms", "broadcast_voice"
  48. private long startupTimestamp;
  49. // Sequencing naming of broadcast that fails to yield its broadcastId
  50. private int unknownBroadcastIdNdx = 1;
  51. private BroadcastException myException;
  52. /**
  53. * Used to communicate media-specific platform resources to broadcasts
  54. */
  55. protected EngineResources resources;
  56. private static Logger myLogger;
  57. private ScheduledExecutorService scheduler;
  58. private long deadBroadcastViewingMinutes;
  59. private int completedJobCount = 0;
  60. private int serviceThreadPoolSize;
  61. private int postbackMaxQueueSize;
  62. private int postbackSenderPoolSize;
  63. private int postbackMaxBatchSize;
  64. protected String runtimeDirPath;
  65. protected String confDirPath;
  66. abstract protected Broadcast mkBroadcast();
  67. public CommEngine(String engineName)
  68. {
  69. this.engineName = engineName;
  70. broadcasts = new HashMap<String, Broadcast>();
  71. startupTimestamp = System.currentTimeMillis();
  72. myException = null;
  73. }
  74. /**
  75. * Relocates a filepath relative to runtime directory if filepath is not absolute.
  76. * @param filepath
  77. * @return
  78. */
  79. public String relocateToRuntimeDir(String filepath)
  80. {
  81. if (filepath.startsWith("/")) return filepath; // no change to absolute path
  82. String relocated = filepath;
  83. // The next 2 lines take care of pre-git era meaning convention of filepath in properties.
  84. // Then, the runtime is relative to the current working directory of tomcat.
  85. // Now they are relative to the runtimDirPath obtained from the tomcat tomcat context.
  86. // These 2 lines an be deleted when all CommEngines in production are in the git era.
  87. String unwanted_prefix = engineName + "/";
  88. if (filepath.startsWith(unwanted_prefix)) relocated = filepath.substring(unwanted_prefix.length());
  89. relocated = runtimeDirPath + "/" + relocated;
  90. return relocated;
  91. }
  92. /**
  93. * Invoked by servlet container during initialization of servlet.
  94. */
  95. public final void init()
  96. {
  97. // check init parameters
  98. ServletContext servletContext = getServletContext();
  99. confDirPath = servletContext.getInitParameter(getConfDirContextName());
  100. System.out.println("Config directory is configured to be '" + confDirPath + "'. Make sure it and its content are readable by user 'tomcat'");
  101. runtimeDirPath = servletContext.getInitParameter(getRunTimeDirContextName());
  102. System.out.println("Runtime directory is configured to be '" + runtimeDirPath + "'. Make sure it and its content are readable by user 'tomcat'");
  103. File propertiesFile = new File(confDirPath + "/properties");
  104. // Configure log4j using log4j.properties file, \
  105. // sibling to the engine properties file.
  106. // This change is backward compatible with placing the log4j.properties file in
  107. // the class path.
  108. String log4j_properties = confDirPath + "/log4j.properties";
  109. // Relocate file property offetting it by the runtimeDirPath
  110. try
  111. {
  112. Properties prop = new Properties();
  113. prop.load(new FileInputStream(log4j_properties));
  114. Enumeration<Object> e = prop.keys();
  115. while (e.hasMoreElements())
  116. {
  117. String key = (String)e.nextElement();
  118. if (key.toLowerCase().endsWith(".file"))
  119. {
  120. String filepath = prop.getProperty(key);
  121. String relocate = relocateToRuntimeDir(filepath);
  122. prop.setProperty(key, relocate);
  123. System.out.println(key + "=" + relocate);
  124. }
  125. }
  126. PropertyConfigurator.configure(prop);
  127. }
  128. catch (Exception e)
  129. {
  130. System.out.println("Failed to configure log4: " + e);
  131. // Do nothing, assuming the exception is FileNotFoundException.
  132. // Remaining log4j initialization will look for log4j.properties
  133. // file in the class path.
  134. }
  135. // This activates Logger class instantiation. At this point, if lo4j
  136. // is not yet configured,
  137. // it will look for the log4j.properties file in the class path.
  138. myLogger = Logger.getLogger(CommEngine.class);
  139. myLogger.info("init() invoked");
  140. // File propertiesFile = new File(
  141. // getClass().getClassLoader().getResource("properties").getFile()
  142. // );
  143. CommonLogger.startup.info("Using lo4j properites file " + log4j_properties);
  144. CommonLogger.startup.info("Using configuration file " + propertiesFile.getAbsolutePath());
  145. config = new Properties();
  146. try
  147. {
  148. config.load(new FileInputStream(propertiesFile));
  149. }
  150. catch (FileNotFoundException e)
  151. {
  152. CommonLogger.alarm.fatal("Properties file " + propertiesFile.getAbsolutePath() + " not found -- abort");
  153. notInService = true;
  154. return;
  155. }
  156. catch (IOException e)
  157. {
  158. CommonLogger.alarm.fatal("Problem in reading properties file " + propertiesFile.getAbsolutePath() + ": " + e.getMessage());
  159. notInService = true;
  160. return;
  161. }
  162. postBackMap = new HashMap<String, PostBack>();
  163. // Set up periodic purge of stale broadcasts, based on deadBroadcastViewingMinutes
  164. String periodStr = config.getProperty("dead_broadcast_viewing_period",
  165. new Long(DEAD_BROADCAST_VIEWING_PERIOD_DEFAULT).toString());
  166. deadBroadcastViewingMinutes = Long.parseLong(periodStr);
  167. CommonLogger.startup.info(String.format("Dead broadcast viewing period: %d minutes", deadBroadcastViewingMinutes));
  168. String str = config.getProperty("service_threadpool_size",
  169. new Integer(SERVICE_THREADPOOL_SIZE_DEFAULT).toString());
  170. serviceThreadPoolSize = Integer.parseInt(str);
  171. CommonLogger.startup.info(String.format("service thread pool size: %d", serviceThreadPoolSize));
  172. String string = config.getProperty("postback_max_queue_size",
  173. new Integer(POSTBACK_MAX_QUEUE_SIZE_DEFAULT).toString());
  174. postbackMaxQueueSize = Integer.parseInt(string);
  175. CommonLogger.activity.info("Postback max queue size = " + postbackMaxQueueSize);
  176. string = config.getProperty("postback_threadpool_size",
  177. new Integer(POSTBACK_THREADPOOL_SIZE_DEFAULT).toString());
  178. postbackSenderPoolSize = Integer.parseInt(string);
  179. CommonLogger.activity.info("Postback threadpool size = " + postbackSenderPoolSize);
  180. string = config.getProperty("postback_max_batch_size",
  181. new Integer(POSTBACK_MAX_BATCH_SIZE_DEFAULT).toString());
  182. postbackMaxBatchSize = Integer.parseInt(string);
  183. CommonLogger.activity.info("Postback max batch size = " + postbackMaxBatchSize);
  184. scheduler = Executors.newScheduledThreadPool(SCHEDULER_THREAD_POOL_SIZE);
  185. scheduler.scheduleAtFixedRate(new Runnable() { public void run() { purgeStaleBroadcasts();}},
  186. deadBroadcastViewingMinutes, deadBroadcastViewingMinutes, TimeUnit.MINUTES);
  187. initChild();
  188. }
  189. protected void purgeStaleBroadcasts()
  190. {
  191. long now = System.currentTimeMillis();
  192. synchronized (broadcasts)
  193. {
  194. for (String id : broadcasts.keySet())
  195. {
  196. if (broadcasts.get(id).changeStateTime - now > deadBroadcastViewingMinutes * 60 * 1000)
  197. {
  198. Broadcast broadcast = broadcasts.get(id);
  199. completedJobCount += broadcast.getCompletedJobCount();
  200. broadcasts.remove(id);
  201. }
  202. }
  203. }
  204. }
  205. /**
  206. *
  207. * @return name of parameter in Tomcat context file, specifying properties file
  208. * for this SMSEngine.
  209. */
  210. protected abstract String getConfDirContextName();
  211. protected abstract String getRunTimeDirContextName();
  212. @Override
  213. protected void doPost(HttpServletRequest request, HttpServletResponse response)
  214. {
  215. Broadcast broadcast = mkBroadcast();
  216. broadcast.doPost(request, response, this);
  217. }
  218. //@Override
  219. // TODO Not used - delete
  220. @Deprecated
  221. protected void doPost_good(HttpServletRequest request, HttpServletResponse response)
  222. {
  223. myException = null;
  224. try
  225. {
  226. Broadcast broadcast = mkBroadcast();
  227. broadcast.setServiceThreadPoolsize(serviceThreadPoolSize);
  228. try
  229. {
  230. broadcast.decode(request, notInService);
  231. if (notInService)
  232. {
  233. throw new PlatformException(PlatformError.RUNTIME_ERROR,
  234. "Not in service");
  235. }
  236. if (broadcast.recipientList.size() == 0)
  237. {
  238. CommonLogger.activity.info("Broadcast " + broadcast.getBroadcastId() + ": No recipients");
  239. broadcast.setState(BroadcastState.COMPLETED, "No recipients", null);
  240. return;
  241. }
  242. // Determine postBackUrl
  243. String postBackURL = broadcast.getPostBackURL();
  244. PostBack postBack = null;
  245. if (postBackURL != null)
  246. {
  247. postBack = postBackMap.get(postBackURL);
  248. if (postBack == null)
  249. {
  250. postBack = new PostBack(postBackURL, broadcast.broadcastType + "_status",
  251. postbackMaxQueueSize, postbackSenderPoolSize, postbackMaxBatchSize);
  252. postBackMap.put(postBackURL, postBack);
  253. }
  254. }
  255. broadcast.initSync(resources);
  256. broadcast.init(postBack);
  257. if (broadcast.getState() == BroadcastState.COMPLETED) return;
  258. }
  259. catch (BroadcastException e)
  260. {
  261. myException = e;
  262. broadcast.setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText);
  263. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  264. myLogger.error("Broadcast aborted", e);
  265. return;
  266. }
  267. catch (Throwable t)
  268. {
  269. // Caught stray unexpected runtime problem
  270. CommonLogger.alarm.error("Broadcast aborted: " + t);
  271. myLogger.error("Broadcast aborted", t);
  272. myException = new BroadcastException(BroadcastError.PLATFORM_ERROR, t.getMessage());
  273. broadcast.setState(BroadcastState.ABORTED, myException.errorCodeText, myException.errorText);
  274. }
  275. finally
  276. {
  277. // Put broadcast in broadcasts map.
  278. String broadcastId = broadcast.getBroadcastId();
  279. if (broadcastId != null && broadcastId.length() != 0)
  280. {
  281. broadcasts.put(broadcastId, broadcast);
  282. }
  283. else
  284. {
  285. String makeUpId = "Unknown" + unknownBroadcastIdNdx++;
  286. broadcasts.put(makeUpId, broadcast);
  287. }
  288. // Return regular or error response
  289. String responseXML = broadcast.getResponseXML(myException);
  290. PrintWriter writer = response.getWriter();
  291. writer.write(responseXML);
  292. writer.close();
  293. }
  294. try
  295. {
  296. broadcast.initAsync();
  297. broadcast.startProcessing();
  298. }
  299. catch (BroadcastException e)
  300. {
  301. broadcast.setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText);
  302. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  303. myLogger.error("Broadcast aborted", e);
  304. }
  305. }
  306. catch (Throwable t)
  307. {
  308. // Caught stray unexpected runtime problem
  309. CommonLogger.alarm.error("Broadcast aborted: " + t.getMessage());
  310. myLogger.error("Broadcast aborted", t);
  311. }
  312. }
  313. /**
  314. * Functions covered are
  315. * get=status
  316. * get=cancel_broadcast&broadcast_id=XXX
  317. */
  318. @Override
  319. protected void doGet(HttpServletRequest request, HttpServletResponse response)
  320. {
  321. PrintWriter out;
  322. try
  323. {
  324. out = response.getWriter();
  325. }
  326. catch (IOException e)
  327. {
  328. CommonLogger.alarm.error("Cannot write a reply: " + e);
  329. return;
  330. }
  331. String get = (String)request.getParameter("get");
  332. if (get == null)
  333. {
  334. // Return http status BAD REQUEST
  335. int httpStatus = HttpServletResponse.SC_BAD_REQUEST;
  336. try
  337. {
  338. response.sendError(httpStatus);
  339. }
  340. catch (IOException e)
  341. {
  342. myLogger.warn("Unnable to return HTTP error code " + httpStatus);
  343. }
  344. return;
  345. }
  346. if (get.equalsIgnoreCase("status"))
  347. {
  348. getStatus(request, out);
  349. }
  350. else if (get.equalsIgnoreCase("cancel_broadcast"))
  351. {
  352. cancelBroadcast(request, out);
  353. }
  354. else if (get.equalsIgnoreCase("pause_broadcast"))
  355. {
  356. pauseBroadcast(request, out);
  357. }
  358. else if (get.equalsIgnoreCase("resume_broadcast"))
  359. {
  360. resumeBroadcast(request, out);
  361. }
  362. else
  363. {
  364. out.write(get + " not supported");
  365. }
  366. out.close();
  367. }
  368. private void cancelBroadcast(HttpServletRequest request, PrintWriter out)
  369. {
  370. // Get broadcastId from request
  371. String broadcastId = getBroadcastId(request);
  372. Broadcast broadcast = broadcasts.get(broadcastId);
  373. if (broadcast == null)
  374. {
  375. out.format("Broadcast %s does not exist", broadcastId);
  376. return;
  377. }
  378. broadcast.cancel(out);
  379. }
  380. protected void pauseBroadcast(HttpServletRequest request, PrintWriter out)
  381. {
  382. // Get broadcastId from request
  383. String broadcastId = getBroadcastId(request);
  384. Broadcast broadcast = broadcasts.get(broadcastId);
  385. if (broadcast == null)
  386. {
  387. out.format("Broadcast %s does not exist", broadcastId);
  388. return;
  389. }
  390. broadcast.pause();
  391. }
  392. protected void resumeBroadcast(HttpServletRequest request, PrintWriter out)
  393. {
  394. // Get broadcastId from request
  395. String broadcastId = getBroadcastId(request);
  396. Broadcast broadcast = broadcasts.get(broadcastId);
  397. if (broadcast == null)
  398. {
  399. out.format("Broadcast %s does not exist", broadcastId);
  400. return;
  401. }
  402. broadcast.resume();
  403. }
  404. /**
  405. * <CallEngine_status>
  406. * status of each broadcast
  407. * <calls><total>ttt</total><connected>nnn</connected>
  408. * </CallEngine_status>
  409. */
  410. private void getStatus(HttpServletRequest request, PrintWriter out)
  411. {
  412. String broadcastId = request.getParameter("broadcast_id");
  413. if (broadcastId != null)
  414. {
  415. broadcastId = broadcastId.trim();
  416. if (broadcastId.length() == 0)
  417. {
  418. out.write("broadcast_id request parameter cannot be empty");
  419. return;
  420. }
  421. Broadcast broadcast = broadcasts.get(broadcastId);
  422. if (broadcast == null)
  423. {
  424. out.write("<error>No such broadcast</error>");
  425. }
  426. else
  427. {
  428. out.write(broadcast.mkStatusReport());
  429. }
  430. return;
  431. }
  432. else
  433. {
  434. String tag = engineName + "_status";
  435. out.write("<" + tag + ">\r\n");
  436. out.write("<startup_time>" + startupTimestamp
  437. + "</startup_time>\r\n");
  438. // First get a copy of broadcasts, to avoid mutex deadlock.
  439. Vector<Broadcast> broadcastList = new Vector<Broadcast>();
  440. synchronized(broadcasts)
  441. {
  442. for (String key : broadcasts.keySet())
  443. {
  444. broadcastList.add(broadcasts.get(key));
  445. }
  446. }
  447. // We have released the lock.
  448. // Then append status of each broadcast to outBuf.
  449. for (Broadcast broadcast : broadcastList)
  450. {
  451. out.write(broadcast.mkStatusReport());
  452. }
  453. out.write("<job_summary completed='" + getCompletedJobCount() + "' ready='" + getReadyJobCount() + "' active='" + getActiveJobCount() + "'/>");
  454. out.write("</" + tag + ">");
  455. }
  456. }
  457. public int getReadyJobCount()
  458. {
  459. int readyCount = 0;
  460. synchronized(broadcasts)
  461. {
  462. for (Broadcast broadcast : broadcasts.values())
  463. {
  464. readyCount += broadcast.getReadyJobCount();
  465. }
  466. }
  467. return readyCount;
  468. }
  469. public int getActiveJobCount()
  470. {
  471. int activeCount = 0;
  472. synchronized(broadcasts)
  473. {
  474. for (Broadcast broadcast : broadcasts.values())
  475. {
  476. activeCount += broadcast.getActiveJobCount();
  477. }
  478. }
  479. return activeCount;
  480. }
  481. public int getCompletedJobCount()
  482. {
  483. int additionalCompletedJobCount = 0;
  484. synchronized(broadcasts)
  485. {
  486. for (Broadcast broadcast : broadcasts.values())
  487. {
  488. additionalCompletedJobCount += broadcast.getCompletedJobCount();
  489. }
  490. }
  491. return completedJobCount + additionalCompletedJobCount;
  492. }
  493. public void removeBroadcast(String broadcastId)
  494. {
  495. CommonLogger.activity.info("Removing broadcast " + broadcastId);
  496. synchronized(broadcasts)
  497. {
  498. broadcasts.remove(broadcastId);
  499. }
  500. }
  501. public boolean notInService()
  502. {
  503. return notInService;
  504. }
  505. /**
  506. * Decode http GET request for broadcast_id value
  507. * @param request
  508. * @return broadcast_id
  509. */
  510. private String getBroadcastId(HttpServletRequest request)
  511. {
  512. return request.getParameter("broadcast_id");
  513. }
  514. /**
  515. * Invoked by servlet container when servlet is destroyed.
  516. */
  517. public final void destroy()
  518. {
  519. System.out.println("Destroying " + engineName);
  520. // Shutdown threads that periodically purge stale broadcasts.
  521. scheduler.shutdownNow();
  522. // Kill threads in each PostBack, which is remembered in postBackMap.
  523. for (PostBack postback : postBackMap.values())
  524. {
  525. postback.terminate();
  526. }
  527. for (Broadcast broadcast : broadcasts.values())
  528. {
  529. broadcast.terminate(BroadcastState.ABORTED, "Platform termination");
  530. }
  531. destroyChild();
  532. super.destroy();
  533. }
  534. /**
  535. * Indirectly invoked by servlet container during servlet initialization.
  536. */
  537. abstract protected void initChild();
  538. /**
  539. * Indirectly invoked by servlet container during destruction of servlet.
  540. */
  541. abstract protected void destroyChild();
  542. public PostBack getPostBack(String postBackURL, String broadcastType) throws BroadcastException
  543. {
  544. if (postBackURL == null) return null;
  545. PostBack postBack = postBackMap.get(postBackURL);
  546. if (postBack != null) return postBack;
  547. try {
  548. postBack = new PostBack(postBackURL, broadcastType + "_status",
  549. postbackMaxQueueSize, postbackSenderPoolSize, postbackMaxBatchSize);
  550. } catch (KeyManagementException | IllegalArgumentException | NoSuchAlgorithmException | KeyStoreException e) {
  551. throw new BroadcastException(BroadcastError.PLATFORM_ERROR, e.getMessage(), e);
  552. }
  553. postBackMap.put(postBackURL, postBack);
  554. return postBack;
  555. }
  556. public EngineResources getResources()
  557. {
  558. return resources;
  559. }
  560. public void addBroadcast(String broadcastId, Broadcast broadcast)
  561. {
  562. if (broadcastId == null) broadcastId = "Unknown" + unknownBroadcastIdNdx++;
  563. broadcasts.put(broadcastId, broadcast);
  564. }
  565. /**
  566. * If broadcast has no id, one will be created for it.
  567. * @param broadcast
  568. */
  569. public void installBroadcast(Broadcast broadcast)
  570. {
  571. String broadcastId = broadcast.getBroadcastId();
  572. if (broadcastId == null) broadcastId = "Unknown" + unknownBroadcastIdNdx++;
  573. broadcasts.put(broadcastId, broadcast);
  574. }
  575. public int getServiceThreadPoolSize()
  576. {
  577. return serviceThreadPoolSize;
  578. }
  579. }