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

1527 行
50 KiB

  1. package altk.comm.engine;
  2. import java.io.IOException;
  3. import java.io.PrintWriter;
  4. import java.io.StringWriter;
  5. import java.time.LocalTime;
  6. import java.util.ArrayList;
  7. import java.util.Arrays;
  8. import java.util.HashMap;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.Queue;
  12. import java.util.concurrent.Executors;
  13. import java.util.concurrent.LinkedBlockingQueue;
  14. import java.util.concurrent.ScheduledExecutorService;
  15. import java.util.concurrent.ScheduledFuture;
  16. import java.util.concurrent.TimeUnit;
  17. import java.util.concurrent.atomic.AtomicInteger;
  18. import javax.servlet.http.HttpServletRequest;
  19. import javax.servlet.http.HttpServletResponse;
  20. import org.apache.log4j.Category;
  21. import org.apache.log4j.Logger;
  22. import org.json.simple.JSONObject;
  23. import com.opencsv.CSVWriter;
  24. import altk.comm.engine.Job.CommJobStatus;
  25. import altk.comm.engine.exception.BroadcastError;
  26. import altk.comm.engine.exception.BroadcastException;
  27. import altk.comm.engine.exception.EngineException;
  28. import altk.comm.engine.exception.PlatformError;
  29. import altk.comm.engine.exception.PlatformException;
  30. /**
  31. * Broadcast class absorbs what was formerly known as Dispatcher class.
  32. *
  33. * @author Yuk-Ming
  34. *
  35. */
  36. public abstract class Broadcast
  37. {
  38. private static final int SCHEDULER_THREAD_POOL_SIZE = 5;
  39. private static final String ACTIVITY_RECORD_ID_PARAM_NAME_DEFAULT = "activity_record_id";
  40. private static final long SLEEP_BETWEEN_JOBS_DEFAULT = 0;
  41. static final String DAILY_STOP_KEY = "daily_stop";
  42. static final String DAILY_START_KEY = "daily_start";
  43. static final String CSV_LOGGER = "csv";
  44. public final String broadcastType;
  45. private String broadcastId;
  46. private BroadcastState state = BroadcastState.ACCEPTED;
  47. private Object stateSemaphore = new Object();
  48. String reason;
  49. String stateErrorText;
  50. public CommEngine commEngine;
  51. public final long receiveTime;
  52. public long serviceStartTime;
  53. public long serviceEndTime;
  54. public long changeStateTime;
  55. protected String activityRecordIdParamName;
  56. private String jobReportRootNodeName;
  57. /**
  58. * Set when reading request XML, but never used.
  59. */
  60. private String launchRecordId;
  61. // protected XPath xpathEngine;
  62. protected String postbackURL;
  63. private Postback postback;
  64. protected String daily_start = "";
  65. protected String daily_stop = "";
  66. /**
  67. * Sleep time in milliseconds between consecutive job processing (actualliy batch)
  68. */
  69. protected long sleepBetweenJobs;
  70. protected static Logger myLogger = Logger.getLogger(Broadcast.class);
  71. protected List<Service> serviceThreadPool;
  72. private Object resumeFlag; // Semaphore for dispatcher threads to resume.
  73. protected List<Recipient> recipientList;
  74. private ScheduledExecutorService scheduler;
  75. private int jobsTotal;
  76. /** Queue of jobs ready to be servivced. Semaphore for next 3 fields */
  77. private Queue<Job> readyQueue;
  78. /** Count of items in scheduler */
  79. private int scheduledJobs;
  80. /** Instantaneous number of jobs being serviced by service provider */
  81. private int serviceActivityCount;
  82. private Integer transactions;
  83. /** Running count of successful jobs */
  84. private AtomicInteger successCount;
  85. private int pauseThreshold;
  86. private int lastPauseCount;
  87. private Logger csvLogger;
  88. public static enum BroadcastState
  89. {
  90. /** Broadcast request accepted for execution */
  91. ACCEPTED,
  92. /** Servicing jobs */
  93. RUNNING,
  94. /** User action, causing system to quiesce, i.e. quiet down */
  95. PAUSING,
  96. /** System is paused and quiet. Ready to resume */
  97. PAUSED,
  98. /** User action */
  99. CANCELING,
  100. /** Ireoverable internal or service provider error */
  101. ABORTING,
  102. /** All servicing done, reporting may still be ongoing */
  103. COMPLETED,
  104. CANCELED(true), // Final state
  105. PURGED(true), // Final state
  106. ABORTED(true), // final state
  107. /** All servicing and reporting done */
  108. ALLDONE(true) // Final state
  109. ;
  110. final public boolean isFinal;
  111. private BroadcastState()
  112. {
  113. isFinal = false;
  114. }
  115. private BroadcastState(boolean isFinal)
  116. {
  117. this.isFinal = isFinal;
  118. }
  119. }
  120. public enum StateChangeStatus
  121. {
  122. SUCCESS,
  123. NO_CHANGE,
  124. FORBIDDEN
  125. }
  126. /**
  127. * When a Broadcast is first created, its state is INSTALLING, during which
  128. * time, the broadcast request XML is digested. After this period, the
  129. * state of the broadcast enters into RUNNING, and it begins with adding
  130. * all VoiceJobs in the request to the Dispatcher's queues. During this period
  131. * calls are dispatched, setup, and terminate. When the last call is terminated,
  132. * the broadcast enters into the COMPLETED state.
  133. *
  134. * Transitions from INSTALLING to RUNNING, and from RUNNING to COMPLETED happen
  135. * automatically without any external influence.
  136. *
  137. * At any time, the broadcast may be canceled by user action, which causes
  138. * the broadcast to transition from the RUNNING state into the CANCELED state.
  139. *
  140. * Since the RUNNING state may go to the COMLETED or to the CANCELED state,
  141. * each due to a different thread, there needs to be a mutex to guarantee
  142. * state and data integrity.
  143. *
  144. * A INSTALLING or RUNNING broadcast may be paused by user action, stopping the
  145. * Dispatcher from making new calls and causing the broadcast to go to
  146. * the HALTED state. Certain error conditions result in the HALTED state.
  147. *
  148. * User may order a broadcast to be removed entirely with a purge command,
  149. * causing the broadcast to go to the PURGED state. This state is there so
  150. * objects, that has reference to a broadcast which has been purged, know
  151. * what to do in that situation.
  152. *
  153. * We need the pause operation to set a RUNNING machine to be in the PAUSING state,
  154. * allow ongoing jobs to proceed to normal termination,
  155. * whereas no new jobs are started. State goes from PAUSING to HALTED.
  156. *
  157. * Cancel-nice operation is pause, followed by the automatic transition
  158. * from HALTED to CANCELED.
  159. *
  160. * Cancel operation forces all jobs to abort, and the state transitions to CANCELED
  161. * immediately.
  162. *
  163. * Because the Dispatcher and Broadcast is one-to-one, and because they access each other's
  164. * data, Dispatcher should be combined into Broadcast.
  165. *
  166. * @author Yuk-Ming
  167. *
  168. */
  169. static Map<BroadcastState, List<BroadcastState>> toStates;
  170. static
  171. {
  172. // Initialize legal transitions of state machine.
  173. // For each state, define a list of legal states that this state can transition to
  174. toStates = new HashMap<BroadcastState, List<BroadcastState>>();
  175. // Transitions from INSTALLING
  176. toStates.put(BroadcastState.ACCEPTED, Arrays.asList(
  177. BroadcastState.RUNNING, // Normal transition
  178. BroadcastState.CANCELING, // User action
  179. BroadcastState.CANCELED, // User action
  180. BroadcastState.PAUSING, // User action
  181. BroadcastState.PAUSED, // User action
  182. BroadcastState.PURGED, // User action
  183. BroadcastState.ABORTING,
  184. BroadcastState.ABORTED,
  185. BroadcastState.ALLDONE // When recipient list is empty
  186. ));
  187. // Transitions from RUNNING
  188. toStates.put(BroadcastState.RUNNING, Arrays.asList(
  189. BroadcastState.COMPLETED, // after completed all sends
  190. BroadcastState.CANCELING, // User action
  191. BroadcastState.CANCELED, // User action
  192. BroadcastState.PAUSING, // User action
  193. BroadcastState.PAUSED, // User action
  194. BroadcastState.PURGED, // User action
  195. BroadcastState.ABORTING,
  196. BroadcastState.ABORTED
  197. ));
  198. // Transitions from CANCELING
  199. toStates.put(BroadcastState.CANCELING, Arrays.asList(
  200. BroadcastState.ABORTING,
  201. BroadcastState.ABORTED,
  202. BroadcastState.COMPLETED,
  203. BroadcastState.CANCELED, // User action
  204. BroadcastState.PURGED // User action
  205. ));
  206. // Transitions from ABORTING
  207. toStates.put(BroadcastState.ABORTING, Arrays.asList(
  208. BroadcastState.ABORTED,
  209. BroadcastState.PURGED // User action
  210. ));
  211. // Transitions from PAUSING
  212. toStates.put(BroadcastState.PAUSING, Arrays.asList(
  213. BroadcastState.RUNNING, // User action
  214. BroadcastState.COMPLETED,
  215. BroadcastState.ABORTING,
  216. BroadcastState.CANCELING, // User action
  217. BroadcastState.CANCELED,
  218. BroadcastState.PAUSED,
  219. BroadcastState.PURGED // User action
  220. ));
  221. // Transitions from PAUSED
  222. toStates.put(BroadcastState.PAUSED, Arrays.asList(
  223. BroadcastState.RUNNING, // User action
  224. BroadcastState.CANCELED, // User action
  225. BroadcastState.CANCELING, // User action
  226. BroadcastState.ABORTING,
  227. BroadcastState.ABORTED,
  228. BroadcastState.PURGED // User action
  229. ));
  230. toStates.put(BroadcastState.COMPLETED, Arrays.asList(
  231. BroadcastState.ALLDONE // when all posting back is complete
  232. ));
  233. }
  234. public static class StateChangeResult
  235. {
  236. public StateChangeStatus stateChangeStatus;
  237. public BroadcastState currentState;
  238. public BroadcastState previousState;
  239. public StateChangeResult(StateChangeStatus stateChangeStatus,
  240. BroadcastState currentState, BroadcastState previousState)
  241. {
  242. this.stateChangeStatus = stateChangeStatus;
  243. this.currentState = currentState;
  244. this.previousState = previousState;
  245. }
  246. }
  247. protected enum PostbackThreadActionOnEmpty
  248. {
  249. CONTINUE,
  250. STOP,
  251. WAIT
  252. }
  253. protected class Service extends Thread
  254. {
  255. Object serviceProviderPeer;
  256. protected Service(String name) throws BroadcastException
  257. {
  258. serviceProviderPeer = getInitializedServiceProviderPeer();
  259. setName(name);
  260. }
  261. public void run()
  262. {
  263. myLogger.info("Thread starting...");
  264. for (;;)
  265. {
  266. if (serviceThreadsShouldPause())
  267. {
  268. synchronized (resumeFlag)
  269. {
  270. try
  271. {
  272. myLogger.debug("Paused");
  273. resumeFlag.wait();
  274. myLogger.debug("Pause ended");
  275. }
  276. catch (InterruptedException e)
  277. {
  278. myLogger.warn("Dispatcher thread interrupted while waiting to resume");
  279. }
  280. }
  281. }
  282. if (serviceThreadsShouldStop()) break;
  283. // Get a batch of jobs, if available
  284. myLogger.debug("Looking for jobs");
  285. List<Job> batch = new ArrayList<Job>();
  286. int batchSize = getJobBatchSize();
  287. synchronized(readyQueue)
  288. {
  289. // We we are to get a batch of more than one, let us fill in the rest.
  290. for (int i = 0; i < batchSize; i++)
  291. {
  292. Job job = readyQueue.poll();
  293. if (job == null) break;
  294. batch.add(job);
  295. }
  296. if (batch.size()== 0)
  297. {
  298. // wait for jobs
  299. try
  300. {
  301. myLogger.debug("Waiting for jobs");
  302. readyQueue.wait();
  303. // go back to look for jobs
  304. continue;
  305. }
  306. catch (Exception e)
  307. {
  308. // go back to look for jobs
  309. continue;
  310. }
  311. }
  312. updateServiceActivityCount(batchSize);
  313. }
  314. // Process jobs.
  315. // Mark start time
  316. long now = System.currentTimeMillis();
  317. for (Job job : batch)
  318. {
  319. job.startTime = now;
  320. }
  321. // Service the jobs
  322. // But first get dependent resource
  323. // which includes allocation from capacity. Only returns when the required allocation
  324. // is obtained. Example, RTP port allocation, limit due to total number of allowable calls.
  325. ServicePrerequisites prerequisites = secureServicePrerequisites();
  326. try
  327. {
  328. int transactions = processJobs(batch, serviceProviderPeer, prerequisites);
  329. incrementTransactions(transactions);
  330. }
  331. catch (EngineException e)
  332. {
  333. // Aborting
  334. myLogger.error("Caught unexpected Exception", e);
  335. setState(BroadcastState.ABORTING, e.errorCodeText, e.errorText);
  336. updateServiceActivityCount(-batchSize);
  337. }
  338. catch (Throwable t)
  339. {
  340. // This is unexpected. Log stack trace
  341. myLogger.error("Caught unexpected Throwable", t);
  342. terminate(BroadcastState.ABORTED, t + ": " + t.getMessage());
  343. updateServiceActivityCount(-batchSize);
  344. }
  345. if (sleepBetweenJobs > 0)
  346. {
  347. try
  348. {
  349. Thread.sleep(sleepBetweenJobs);
  350. }
  351. catch (InterruptedException e1)
  352. {
  353. // Do nothing?
  354. }
  355. }
  356. }
  357. // Exit thread
  358. myLogger.info("Thread terminating");
  359. synchronized(readyQueue)
  360. {
  361. readyQueue.notify();
  362. }
  363. System.out.println(getName() + " terminating");
  364. closeServiceProvider(serviceProviderPeer);
  365. }
  366. }
  367. /**
  368. *
  369. * @param broadcastType
  370. * @param activityRecordIdParamName - if null, default is used.
  371. * @param jobReportRootNodeName
  372. */
  373. protected Broadcast(String broadcastType, String activityRecordIdParamName, String jobReportRootNodeName)
  374. {
  375. this.broadcastType = broadcastType;
  376. this.activityRecordIdParamName = activityRecordIdParamName == null? ACTIVITY_RECORD_ID_PARAM_NAME_DEFAULT : activityRecordIdParamName;
  377. this.jobReportRootNodeName = jobReportRootNodeName;
  378. postback = null;
  379. successCount = new AtomicInteger(0);
  380. sleepBetweenJobs = SLEEP_BETWEEN_JOBS_DEFAULT;
  381. readyQueue = new LinkedBlockingQueue<Job>();
  382. serviceThreadPool = new ArrayList<Service>();
  383. recipientList = new ArrayList<Recipient>();
  384. scheduler = Executors.newScheduledThreadPool(SCHEDULER_THREAD_POOL_SIZE);
  385. resumeFlag = new Object();
  386. receiveTime = System.currentTimeMillis();
  387. serviceActivityCount = Integer.valueOf(0);
  388. transactions = Integer.valueOf(0);
  389. lastPauseCount = 0;
  390. csvLogger = Logger.getLogger(CSV_LOGGER);
  391. }
  392. private void incrementTransactions(int delta)
  393. {
  394. synchronized (transactions)
  395. {
  396. transactions += delta;
  397. lastPauseCount += delta;
  398. }
  399. if (pauseThreshold > 0 && lastPauseCount >= pauseThreshold)
  400. {
  401. pause(null, null);
  402. }
  403. }
  404. /**
  405. * Experimental formulation where it takes over directing
  406. * the activity of a Broadcast, as it should, instead of relegating
  407. * it to CommEngine. This is directly invoked by CommEngine.doPost method,
  408. * and is much easier to read and comprehend.
  409. * <p>It is responsible for
  410. * <ul>
  411. * <li>Replying with HTTP response, and closing HTTP request and response
  412. * <li>Does broadcast and posts real-time progress, using post back queues
  413. * from CommEngine.
  414. * </ul>
  415. * Strategy of execution:
  416. * <ul>
  417. * <li>Decode xml request
  418. * <li>Set up Service threads
  419. * <li> Invite derived class to set up contexts, one for each of the Service threads.
  420. * <li>Do broadcast.
  421. * </ul>
  422. * This method is to be invoked by the CommEngine.doPost method, which fields a HTTP POST.
  423. * <p>
  424. * Adopting the use of this method by CommEngine
  425. * is 100% compatible with derived classes VoiceBroadcast and EmailBroadcast
  426. * existing today, 6/18/2016.
  427. *
  428. *
  429. * @param request
  430. * @param response
  431. * @param commEngine
  432. */
  433. protected void doPost(HttpServletRequest request, HttpServletResponse response,
  434. CommEngine commEngine)
  435. {
  436. myLogger.debug("Entering Broadcast.doPost method");
  437. BroadcastException myException = null;
  438. this.commEngine = commEngine;
  439. pauseThreshold = commEngine.getPauseThreshold();
  440. try
  441. {
  442. // Check validity of operating hours parameters
  443. boolean notInService = commEngine.notInService();
  444. decode(request, notInService);
  445. // Now that have decoded the id of this broadcast,
  446. // ask CommEngine to install it with its id.
  447. commEngine.installBroadcast(this);
  448. setOperatingHours(DAILY_START_KEY, daily_start);
  449. setOperatingHours(DAILY_STOP_KEY, daily_stop);
  450. if (notInService)
  451. {
  452. throw new PlatformException(PlatformError.RUNTIME_ERROR,
  453. "Not in service");
  454. }
  455. if (recipientList.size() == 0)
  456. {
  457. CommonLogger.activity.info("Broadcast " + getBroadcastId() + ": No recipients");
  458. setState(BroadcastState.ALLDONE, "No recipients", null);
  459. return;
  460. }
  461. initSync(commEngine.getResources());
  462. for (Recipient recipient : recipientList)
  463. {
  464. readyQueue.add(mkJob(recipient));
  465. }
  466. if (state == BroadcastState.ALLDONE) return;
  467. }
  468. catch (BroadcastException e)
  469. {
  470. myException = e;
  471. setState(BroadcastState.ABORTING, e.errorCodeText, e.errorText);
  472. CommonLogger.alarm.error("Broadcast aborting: " + e.getMessage());
  473. myLogger.error("Broadcast aborted", e);
  474. return;
  475. }
  476. catch (Throwable t)
  477. {
  478. // Caught stray unexpected runtime problem
  479. CommonLogger.alarm.error("Broadcast aborted: " + t);
  480. myLogger.error("Broadcast aborted", t);
  481. myException = new BroadcastException(BroadcastError.PLATFORM_ERROR, t.getMessage());
  482. setState(BroadcastState.ABORTED, myException.errorCodeText, myException.errorText);
  483. }
  484. finally
  485. {
  486. // Return regular or error response
  487. String responseXML = getResponseXML(myException);
  488. PrintWriter writer;
  489. try
  490. {
  491. writer = response.getWriter();
  492. writer.write(responseXML);
  493. writer.close();
  494. }
  495. catch (IOException e)
  496. {
  497. myLogger.error("Failed to write reponse to requester. Aborts broadcast." );
  498. if (state != BroadcastState.ABORTED)
  499. {
  500. setState(BroadcastState.ABORTED, "Failed to reply to requester", e.getMessage());
  501. }
  502. return;
  503. }
  504. if (myException != null) return;
  505. }
  506. // So far so good, we now go ahead with completing
  507. // initialization.
  508. try
  509. {
  510. jobsTotal = recipientList.size();
  511. postback = new Postback(this,
  512. getPostbackMaxQueueSize(),
  513. getPostbackSenderPoolSize(),
  514. getPostbackMaxBatchSize());
  515. initAsync();
  516. // Create service thread pool to dispatch jobs,
  517. // at the same time, setting up a list of service thread names
  518. // for use by derived class to set up contexts in which
  519. // these threads run.
  520. int serviceThreadPoolSize = getServiceThreadPoolSize();
  521. myLogger.debug("At creating service threads, serviceThreadPoolSize = " + serviceThreadPoolSize);
  522. List<String> serviceThreadNames = new ArrayList<String>();
  523. for (int i = 0; i < serviceThreadPoolSize; i++)
  524. {
  525. String threadName = broadcastId + "-service-thread." + i;
  526. Service serviceThread = new Service(threadName);
  527. serviceThreadPool.add(serviceThread);
  528. serviceThreadNames.add(threadName);
  529. }
  530. doBroadcast();
  531. }
  532. catch (BroadcastException e)
  533. {
  534. setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText);
  535. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  536. myLogger.error("Broadcast aborted", e);
  537. }
  538. catch (Exception e)
  539. {
  540. setState(BroadcastState.ABORTED, BroadcastError.UNEXPECTED_EXCEPTION.toString(), e.getMessage());
  541. CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage());
  542. myLogger.error("Broadcast aborted", e);
  543. }
  544. finally
  545. {
  546. destroyResources();
  547. if (postback != null)
  548. {
  549. postback.wrapup();
  550. postback = null;
  551. }
  552. CommonLogger.activity.info("Broadcast " + getId() + " terminated");
  553. }
  554. }
  555. protected int getPostbackMaxQueueSize()
  556. {
  557. return commEngine.getPostbackMaxQueueSize();
  558. }
  559. protected int getPostbackMaxBatchSize()
  560. {
  561. return commEngine.getPostbackMaxBatchSize();
  562. }
  563. protected int getServiceThreadPoolSize()
  564. {
  565. return commEngine.getServiceThreadPoolSize();
  566. }
  567. protected int getPostbackSenderPoolSize()
  568. {
  569. return commEngine.getPostbackSenderPoolSize();
  570. }
  571. protected abstract void returnPrerequisites(ServicePrerequisites prerequisites);
  572. /**
  573. * Creates and initializes a service provider, to be used by only one service thread.
  574. * If service provider is not thread-specific, then this method may return null, and
  575. * a common service provider is created outside of this method.
  576. *
  577. * @return service provider as a class Object instance.
  578. * @throws BroadcastException
  579. */
  580. protected abstract Object getInitializedServiceProviderPeer() throws BroadcastException;
  581. /**
  582. * Obtains the required components to support a service; e.g. RTP port, or a place
  583. * in maximum total number of calls. Does not return till the reequired prerequisites are obtained.
  584. * @return null, if no prerequisite is required, as in the case of email and sms engines.
  585. */
  586. abstract protected ServicePrerequisites secureServicePrerequisites();
  587. abstract public void closeServiceProvider(Object serviceProvider);
  588. /**
  589. * Makes a state transition to the given newState if the transition from
  590. * the current state is legal. Also posts back a state change notification.
  591. * @param newState
  592. * @return StateChangeResult
  593. */
  594. public StateChangeResult setState(BroadcastState newState)
  595. {
  596. return setState(newState, null, null);
  597. }
  598. /**
  599. * Makes a state transition to the given newState if the transition from
  600. * the current state is legal. Also posts back a state change notification.
  601. * @param newState
  602. * @return StateChangeResult
  603. */
  604. public synchronized StateChangeResult setState(BroadcastState newState,
  605. String reason, String stateErrorText)
  606. {
  607. boolean isLegal;
  608. BroadcastState prev = null;
  609. if (state == newState) return new StateChangeResult(StateChangeStatus.NO_CHANGE, state, null);
  610. synchronized(stateSemaphore)
  611. {
  612. List<BroadcastState> to = toStates.get(state);
  613. isLegal = (to == null? false : to.contains(newState));
  614. prev = state;
  615. if (isLegal)
  616. {
  617. this.reason = reason;
  618. this.stateErrorText = stateErrorText;
  619. CommonLogger.activity.info(String.format("Broadcast %s: State transitioned from %s to %s", broadcastId, state, newState));
  620. state = newState;
  621. changeStateTime = System.currentTimeMillis();
  622. if (state == BroadcastState.RUNNING) serviceStartTime = changeStateTime;
  623. if (prev == BroadcastState.RUNNING) serviceEndTime = changeStateTime;
  624. if (postback != null)
  625. {
  626. if (state == BroadcastState.ALLDONE) postback.queueReport(mkStatusReport());
  627. else postback.queueReportFirst(mkStatusReport());
  628. }
  629. return new StateChangeResult(StateChangeStatus.SUCCESS, newState, prev);
  630. }
  631. else
  632. {
  633. // log illegal state transition with call trace
  634. Exception e = new Exception(String.format("Broadast %s ignored illegal transition from %s to %s", broadcastId, prev, newState));
  635. myLogger.error(e.getMessage());
  636. myLogger.debug("This exception is not thrown -- only for debugging information", e);
  637. return new StateChangeResult(StateChangeStatus.FORBIDDEN, prev, null);
  638. }
  639. }
  640. }
  641. protected void setBroadcastId(String broadcastId)
  642. {
  643. if (broadcastId == null)
  644. throw new IllegalArgumentException(
  645. "Argument broadcastId in Broadcast.setBroadcastId method cannot be null");
  646. if (this.broadcastId != null)
  647. throw new IllegalStateException(
  648. "Broadcast.setBroadcastId method cannot be invoked more than once for a Broadcast");
  649. this.broadcastId = broadcastId;
  650. }
  651. protected void setLaunchRecordId(String launchRecordId)
  652. {
  653. this.launchRecordId = launchRecordId;
  654. }
  655. public String getBroadcastId()
  656. {
  657. return broadcastId;
  658. }
  659. public String getLaunchRecordId()
  660. {
  661. return launchRecordId;
  662. }
  663. /**
  664. *
  665. * @param e
  666. * @return HTTP response for normal case (when exception e is null), or with exception
  667. */
  668. public String getResponseXML(BroadcastException e)
  669. {
  670. String tagName = broadcastType + "_response";
  671. StringBuffer responseXML = new StringBuffer("<" + tagName);
  672. if (broadcastId != null && broadcastId.length() > 0)
  673. {
  674. responseXML.append(" broadcast_id=\"");
  675. responseXML.append(broadcastId);
  676. responseXML.append("\"");
  677. }
  678. responseXML.append(" accepted='");
  679. responseXML.append(e != null || state == BroadcastState.COMPLETED ? "FALSE" : "TRUE");
  680. responseXML.append("'");
  681. if (e == null)
  682. {
  683. if (reason != null && reason.length() > 0)
  684. {
  685. responseXML.append(" error='" + reason + "'");
  686. }
  687. responseXML.append('>');
  688. }
  689. else
  690. {
  691. if (e.errorCode != null)
  692. {
  693. responseXML.append(" error='");
  694. responseXML.append(e.errorCode.toString());
  695. responseXML.append("'");
  696. }
  697. responseXML.append('>');
  698. if (e.errorText != null)
  699. {
  700. responseXML.append("<error_text>");
  701. responseXML.append(e.errorText);
  702. responseXML.append("</error_text>");
  703. }
  704. }
  705. responseXML.append("</" + tagName + '>');
  706. return responseXML.toString();
  707. }
  708. public String getPostbackURL()
  709. {
  710. return postbackURL;
  711. }
  712. protected String mkResponseXML(String errorCode, String errorText)
  713. {
  714. String tagName = broadcastType + "_response";
  715. StringBuffer responseXML = new StringBuffer("<" + tagName);
  716. String broadcastId = getBroadcastId();
  717. if (broadcastId != null && broadcastId.length() > 0)
  718. {
  719. responseXML.append(" broadcast_id=\"");
  720. responseXML.append(broadcastId);
  721. responseXML.append("\"");
  722. }
  723. responseXML.append(" accepted='");
  724. responseXML.append(errorCode == null ? "TRUE" : "FALSE");
  725. responseXML.append("'");
  726. if (errorCode == null)
  727. {
  728. responseXML.append('>');
  729. }
  730. else
  731. {
  732. responseXML.append(" error='");
  733. responseXML.append(errorCode);
  734. responseXML.append("'");
  735. responseXML.append('>');
  736. if (errorText != null)
  737. {
  738. responseXML.append("<error_text>");
  739. responseXML.append(errorText.replaceAll("\\&", "&amp;")
  740. .replaceAll("<", "&lt;"));
  741. responseXML.append("</error_text>");
  742. }
  743. }
  744. responseXML.append("</" + tagName + '>');
  745. return responseXML.toString();
  746. }
  747. /**
  748. * If finalState is final, then this state is set, and dispatcher threads are stopped.
  749. * Overriding implementation may release all other resources, like timers.
  750. * @param finalState
  751. */
  752. public void terminate(BroadcastState finalState, String reason)
  753. {
  754. if (!finalState.isFinal) throw new IllegalArgumentException("Argument finalState " + finalState + " in Broadcast.terminate method is not final");
  755. StateChangeResult result = setState(finalState, reason, null);
  756. switch (result.stateChangeStatus)
  757. {
  758. case SUCCESS:
  759. break;
  760. case NO_CHANGE:
  761. return;
  762. case FORBIDDEN:
  763. myLogger.error("Not allow to terminate broadcast in " + result.currentState + " state");
  764. return;
  765. default: // Should not happen
  766. return;
  767. }
  768. // Wake up all dispatcher threads waiting on readyQueue so they will all stop
  769. synchronized(readyQueue)
  770. {
  771. readyQueue.notifyAll();
  772. }
  773. // Wake up all sleeping dispatcher threads for same reason.
  774. for(Thread t : serviceThreadPool)
  775. {
  776. try
  777. {
  778. t.interrupt();
  779. }
  780. catch (Exception e)
  781. {
  782. myLogger.warn("Interrupted while waiting for Thread " + t.getName() + " to terminate");
  783. }
  784. }
  785. // Quiesce scheduler, and terminate it.
  786. scheduler.shutdownNow();
  787. }
  788. /**
  789. * Defaults to current state
  790. * @return XML string
  791. */
  792. protected String mkStatusReport()
  793. {
  794. StringBuffer statusBf = new StringBuffer();
  795. String topLevelTag = broadcastType;
  796. String broadcastId = getBroadcastId();
  797. if (broadcastId == null) broadcastId = "";
  798. statusBf.append("<" + topLevelTag + " broadcast_id='" + broadcastId
  799. + "' receive_time='" + receiveTime
  800. + "' recipient_count='" + recipientList.size() + "'");
  801. if (launchRecordId != null)
  802. {
  803. statusBf.append(" launch_record_id='" + launchRecordId + "'>");
  804. }
  805. statusBf.append("<current_time>" + System.currentTimeMillis() + "</current_time>");
  806. if (serviceStartTime > 0) statusBf.append("<service_start_time>" + serviceStartTime
  807. + "</service_start_time>");
  808. if (state.isFinal && serviceEndTime > 0) statusBf.append("<service_end_time>" + serviceEndTime
  809. + "</service_end_time>");
  810. statusBf.append("<state>" + state + "</state><state_change_time>" + changeStateTime
  811. + "</state_change_time>");
  812. statusBf.append("<reason>" + (reason == null? "" : Util.xmlEscape(reason))
  813. + "</reason>");
  814. statusBf.append("<error_text>" + (stateErrorText == null? "" : Util.xmlEscape(stateErrorText))
  815. + "</error_text>");
  816. statusBf.append("<transactions>" + transactions + "</transactions>");
  817. statusBf.append("<success>" + successCount.intValue() + "</success>");
  818. statusBf.append("<job_summary completed='" + getCompletedJobCount() +
  819. "' ready='" + getPendingJobCount() + "'");
  820. statusBf.append(" active='" + getActiveJobCount() + "'");
  821. statusBf.append("></job_summary>\n");
  822. statusBf.append("<daily_stop>" + daily_stop + "</daily_stop>");
  823. statusBf.append("<daily_start>" + daily_start + "</daily_start>\n");
  824. statusBf.append(additionalStatusXML());
  825. statusBf.append("</" + topLevelTag + ">");
  826. String statusReport = statusBf.toString();
  827. return statusReport;
  828. }
  829. /**
  830. * Derived class may add additional status in a broadcast status XML posted back to portal,
  831. * by returning an XML tag which will be included immediately in the top-level tag
  832. * of the broadcast status.
  833. */
  834. protected String additionalStatusXML()
  835. {
  836. return "";
  837. }
  838. /**
  839. *
  840. * @return instantaneous number of jobs being serviced by service provider
  841. */
  842. protected int getActiveJobCount()
  843. {
  844. return serviceActivityCount;
  845. }
  846. /**
  847. * Parses broadcastId and return if notInService is true.
  848. * Otherwise, continue parsing postBackUrl, recipientList,
  849. * and implementation-specific data from request.
  850. * Avoid throwing an exception before parsing and setting broadcastId.
  851. * @param notInService
  852. * @throws EngineException
  853. */
  854. protected abstract void decode(HttpServletRequest request, boolean notInService)
  855. throws EngineException;
  856. protected abstract void initSync(EngineResources resources) throws BroadcastException;
  857. protected Job mkJob(Recipient recipient)
  858. {
  859. return new Job(recipient);
  860. }
  861. /**
  862. * Overriding implementation performs time consuming initialization, after returning
  863. * POST http status indicating accepting broadcast for processing.
  864. *
  865. * @throws BroadcastException - to abort broadcast
  866. */
  867. protected void initAsync() throws BroadcastException
  868. {
  869. // Do nothing in base class.
  870. }
  871. public String getId()
  872. {
  873. return broadcastId;
  874. }
  875. /**
  876. * Sets the stateMachine to CANCEL
  877. * @param reasons - may be null.
  878. */
  879. protected void cancel(String reason, PrintWriter out)
  880. {
  881. BroadcastState targetState = getActiveJobCount() == 0?
  882. BroadcastState.CANCELED : BroadcastState.CANCELING;
  883. StateChangeResult result = setState(targetState, reason, null);
  884. String responseContent = null;
  885. switch (result.stateChangeStatus)
  886. {
  887. case SUCCESS:
  888. responseContent = "Broadcast is being canceled";
  889. break;
  890. case NO_CHANGE:
  891. responseContent = "Already canceled";
  892. break;
  893. case FORBIDDEN:
  894. responseContent = "Not canceled: Not allowed to cancel a broadcast in " + result.currentState + " state";
  895. }
  896. out.write(responseContent);
  897. wakeUpServiceThreads();
  898. }
  899. /**
  900. *
  901. * @param reason
  902. * @param out
  903. */
  904. protected void pause(String reason, PrintWriter out)
  905. {
  906. if (state == BroadcastState.ACCEPTED || state.isFinal) return;
  907. // Sets state to PAUSING, which is monitored by Broadcast.Service threads.
  908. // EVentually, when all service activity ends, the state transitions to PAUSED
  909. StateChangeResult result = setState(BroadcastState.PAUSING, reason, null);
  910. switch (result.stateChangeStatus)
  911. {
  912. case FORBIDDEN:
  913. if (out != null) out.write("pause not allowed");
  914. break;
  915. case SUCCESS:
  916. lastPauseCount = 0;
  917. if (out != null) out.write("Broadcast is being PAUSED");
  918. break;
  919. case NO_CHANGE:
  920. if (out != null) out.write("Broadcast is already RUNNING");
  921. }
  922. }
  923. protected void resume(String reason, PrintWriter out)
  924. {
  925. if (state == BroadcastState.ACCEPTED || state.isFinal) return;
  926. if (!withinOperatingHours())
  927. {
  928. if (out != null) out.write("Cannot resume outside operating hours");
  929. return;
  930. }
  931. synchronized (resumeFlag)
  932. {
  933. StateChangeResult result = setState(BroadcastState.RUNNING, reason, null);
  934. switch (result.stateChangeStatus)
  935. {
  936. case FORBIDDEN:
  937. if (out != null) out.write("resume not allowed");
  938. break;
  939. case SUCCESS:
  940. if (out != null) out.write("Broadcast resumed");
  941. resumeFlag.notifyAll();
  942. break;
  943. default:
  944. break;
  945. }
  946. }
  947. }
  948. /**
  949. * Derived class may make its own Implementation of JobReport
  950. * @return
  951. */
  952. protected JobReport mkJobReport()
  953. {
  954. return new JobReport();
  955. }
  956. public void addJob(Job job)
  957. {
  958. synchronized(readyQueue)
  959. {
  960. readyQueue.add(job);
  961. readyQueue.notifyAll();
  962. }
  963. }
  964. /**
  965. * For use by the new Broadcast.doPost method.
  966. * It changes state to RUNNING and waits for all Service threads
  967. * to terminate after starting them.
  968. * @throws BroadcastException
  969. */
  970. public void doBroadcast() throws BroadcastException
  971. {
  972. changeStateTime = System.currentTimeMillis();
  973. if (!serviceThreadsShouldStop())
  974. {
  975. if (withinOperatingHours())
  976. {
  977. setState(BroadcastState.RUNNING);
  978. }
  979. else
  980. {
  981. setState(BroadcastState.PAUSED, "clock", null);
  982. }
  983. // Start the dispatcher threads
  984. for (Service thread : serviceThreadPool)
  985. {
  986. thread.start();
  987. }
  988. // Wait for them to finish
  989. for (Service thread : serviceThreadPool)
  990. {
  991. try
  992. {
  993. thread.join();
  994. }
  995. catch (InterruptedException e)
  996. {
  997. myLogger.error("Caught exception while waiting for a Service thread to terminate:" + e);
  998. }
  999. }
  1000. waitForEndOfService();
  1001. }
  1002. }
  1003. private boolean withinOperatingHours() {
  1004. if (daily_start == null || daily_start.trim().length() == 0) return true;
  1005. int dailyStartMin = convert2Min(daily_start);
  1006. if (daily_stop == null || daily_stop.trim().length() == 0) return true;
  1007. int dailyStopMin = convert2Min(daily_stop);
  1008. // Ensure daily stop > daily start
  1009. if (dailyStopMin < dailyStartMin) dailyStopMin += 24 * 60;
  1010. LocalTime now = LocalTime.now();
  1011. int nowMin = now.getHour() * 60 + now.getMinute();
  1012. if (nowMin < dailyStartMin) nowMin += 24 * 60;
  1013. boolean within = nowMin >= dailyStartMin && nowMin < dailyStopMin;
  1014. return within;
  1015. }
  1016. private int convert2Min(String hhmm) {
  1017. String[] parts = hhmm.split(":");
  1018. int hh = Integer.parseInt(parts[0]);
  1019. int mm = Integer.parseInt(parts[1]);
  1020. return hh * 60 + mm;
  1021. }
  1022. /**
  1023. * Derived class should wait for end of service before returning.
  1024. * At this point all service threads have already ended. If the derived
  1025. * class has other threads still taking part in providing service, wait for
  1026. * them to terminate.
  1027. */
  1028. protected void waitForEndOfService() {}
  1029. /**
  1030. * Derived class destroy resources needed for providing service
  1031. */
  1032. protected void destroyResources() {}
  1033. /**
  1034. * Derived may release resources here.
  1035. */
  1036. protected void close()
  1037. {
  1038. myLogger.debug("In close()");;
  1039. postback.wrapup();
  1040. postback = null;
  1041. }
  1042. /**
  1043. * Derived class may set up environment before starting Service threads.
  1044. * @param serviceThreadNames
  1045. */
  1046. protected void initServiceThreadContexts()
  1047. {
  1048. // Do nothing in base class
  1049. }
  1050. /**
  1051. * Experimental - needed to go with the also experimental method exec.
  1052. * @param serviceThreadNames
  1053. */
  1054. protected void initServiceThreadContexts(List<String> serviceThreadNames)
  1055. {
  1056. // Do nothing in base class
  1057. }
  1058. /**
  1059. * Examines if service threads should stop running, or even start
  1060. *
  1061. * @return
  1062. */
  1063. private boolean serviceThreadsShouldStop()
  1064. {
  1065. if (state == BroadcastState.CANCELING ||
  1066. state == BroadcastState.ABORTING ||
  1067. state.isFinal)
  1068. {
  1069. return true;
  1070. }
  1071. if (getRemainingJobCount() == 0)
  1072. {
  1073. wakeUpServiceThreads();
  1074. return true;
  1075. }
  1076. return false;
  1077. }
  1078. private void wakeUpServiceThreads()
  1079. {
  1080. synchronized (readyQueue)
  1081. {
  1082. readyQueue.notifyAll();
  1083. }
  1084. synchronized (resumeFlag)
  1085. {
  1086. resumeFlag.notifyAll();
  1087. }
  1088. }
  1089. private boolean serviceThreadsShouldPause()
  1090. {
  1091. return state == BroadcastState.PAUSED || state == BroadcastState.PAUSING;
  1092. }
  1093. /**
  1094. * job status is reported back to this broadcast, via the logAndQueueForPostBack method.
  1095. * This method should use the updateServiceActivityCount(+-1) method to allow Broadcast
  1096. * to keep track of overall service progress. The serviceActvityCount is used to determine
  1097. * if all service threads are idle or terminated.
  1098. * @param batch
  1099. * @param prerequisites
  1100. * @return int - number of transactions employed to service these jobs.
  1101. * @throw Exception to abort broadcast
  1102. */
  1103. abstract protected int processJobs(List<Job> batch, Object serviceProvider, ServicePrerequisites prerequisites)
  1104. throws EngineException;
  1105. /**
  1106. * Size of a batch of jobs to be processed together. For email, this may be more than 1,
  1107. * and this method should be overridden.
  1108. * @return size of a batch of jobs to be processed together.
  1109. */
  1110. protected int getJobBatchSize()
  1111. {
  1112. return 1;
  1113. }
  1114. protected void updateServiceActivityCount(int increment)
  1115. {
  1116. synchronized (readyQueue)
  1117. {
  1118. serviceActivityCount += increment;
  1119. myLogger.debug("serviceActivityCount = " + serviceActivityCount );
  1120. if (increment < 0 && serviceActivityCount <= 0)
  1121. {
  1122. if (state == BroadcastState.RUNNING
  1123. || state == BroadcastState.PAUSING
  1124. || state == BroadcastState.CANCELING
  1125. )
  1126. {
  1127. if (getRemainingJobCount() == 0) {
  1128. setState(BroadcastState.COMPLETED);
  1129. }
  1130. }
  1131. }
  1132. }
  1133. }
  1134. /**
  1135. * Sets jobStatus in job, and post job report.
  1136. * If jobStatus is final, and no rescheduling,
  1137. * then decrement number of remainingJobs,and increment completedJobCount.
  1138. * @param job
  1139. * @param jobStatus
  1140. * @param errorText
  1141. */
  1142. public void postJobStatus(Job job)
  1143. {
  1144. if (job.isBroadcastFatal()) setState(BroadcastState.ABORTING, job.getErrorText(), null);
  1145. if (job.jobStatus == CommJobStatus.SUCCESS) successCount.incrementAndGet();
  1146. if (postback != null)
  1147. {
  1148. JobReport report = mkJobReport();
  1149. report.initBase(job, broadcastId, launchRecordId, activityRecordIdParamName, jobReportRootNodeName);
  1150. report.init(job);
  1151. postback.queueReport(report.toString());
  1152. if (job.statusIsFinal()) updateServiceActivityCount(-1);
  1153. }
  1154. }
  1155. /**
  1156. * Logs completedJobCount, readyQueue.size(),
  1157. * active job count, and total.
  1158. * Job statistics are collected by length of readyQueue, completedJobCount,
  1159. */
  1160. private void logJobCount(String title)
  1161. {
  1162. if (postback == null) {
  1163. myLogger.debug(title + ": postback = null");
  1164. myLogger.debug(String.format("%s: state %s, completed: %d, active: %d, ready: %d, scheduled: %d, total jobs: %d, remaining: %d, postQueue: ",
  1165. title,
  1166. state,
  1167. getCompletedJobCount(),
  1168. getActiveJobCount(),
  1169. readyQueue.size(),
  1170. scheduledJobs,
  1171. jobsTotal,
  1172. getRemainingJobCount()
  1173. ));
  1174. return;
  1175. }
  1176. if (postback.postQueue == null) {
  1177. myLogger.debug(title + ": postback.postQueue = null");
  1178. myLogger.debug(String.format("%s: state %s, completed: %d, active: %d, ready: %d, scheduled: %d, total jobs: %d, remaining: %d, postQueue: ",
  1179. title,
  1180. state,
  1181. getCompletedJobCount(),
  1182. getActiveJobCount(),
  1183. readyQueue.size(),
  1184. scheduledJobs,
  1185. jobsTotal,
  1186. getRemainingJobCount()
  1187. ));
  1188. return;
  1189. }
  1190. myLogger.debug(String.format("%s: state %s, completed: %d, active: %d, ready: %d, scheduled %d, total jobs: %d, remaining: %d, postQueue: %d",
  1191. title,
  1192. state,
  1193. getCompletedJobCount(),
  1194. getActiveJobCount(),
  1195. readyQueue.size(),
  1196. scheduledJobs,
  1197. jobsTotal,
  1198. getRemainingJobCount(),
  1199. postback.postQueue.size()
  1200. ));
  1201. }
  1202. /**
  1203. * Number of jobs to be completed.
  1204. * @return
  1205. */
  1206. private int getRemainingJobCount()
  1207. {
  1208. synchronized(readyQueue) {
  1209. return readyQueue.size() + scheduledJobs;
  1210. }
  1211. }
  1212. public ScheduledFuture<?> rescheduleJob(final Job job, long rescheduleTimeMS)
  1213. {
  1214. if (rescheduleTimeMS < 0) return null;
  1215. // No more rescheduling on cancel, abort, or alldone
  1216. if (state == BroadcastState.CANCELING
  1217. || state == BroadcastState.CANCELED
  1218. || state == BroadcastState.ABORTED
  1219. || state == BroadcastState.ABORTING
  1220. || state == BroadcastState.ALLDONE
  1221. )
  1222. {
  1223. return null;
  1224. }
  1225. job.errorText = "";
  1226. if (rescheduleTimeMS == 0)
  1227. {
  1228. addJob(job);
  1229. return null;
  1230. }
  1231. synchronized(readyQueue) {
  1232. scheduledJobs++;
  1233. }
  1234. Runnable r = new Runnable() { public void run() {
  1235. synchronized(readyQueue) {
  1236. scheduledJobs--;
  1237. addJob(job);
  1238. }
  1239. }
  1240. };
  1241. return scheduler.schedule(r, rescheduleTimeMS, TimeUnit.MILLISECONDS);
  1242. }
  1243. public BroadcastState getState()
  1244. {
  1245. return state;
  1246. }
  1247. public int getPendingJobCount()
  1248. {
  1249. switch (state)
  1250. {
  1251. case RUNNING:
  1252. case PAUSING:
  1253. case PAUSED:
  1254. return getRemainingJobCount();
  1255. default:
  1256. return 0;
  1257. }
  1258. }
  1259. public int getCompletedJobCount()
  1260. {
  1261. synchronized(readyQueue)
  1262. {
  1263. return jobsTotal - getRemainingJobCount() - serviceActivityCount;
  1264. }
  1265. }
  1266. public String getBroadcastType() {
  1267. return broadcastType;
  1268. }
  1269. /**
  1270. * To be invoked only when postQueue is empty.
  1271. * Side effect: sets broadcast state from PAUSING to PAUSE
  1272. * when there are no active jobs being processed.
  1273. * Similarly from CANCELING to CANCELED, from ABORTING to ABORTED, from COMPLETED to ALLDONE
  1274. * @return
  1275. */
  1276. public PostbackThreadActionOnEmpty getPostbackThreadActionOnEmpty() {
  1277. myLogger.debug(String.format(
  1278. "getPostbackThreadActionOnEmpty(): broadcast state %s, serviceActivityCount %d",
  1279. state, serviceActivityCount));
  1280. if (state.isFinal) return PostbackThreadActionOnEmpty.STOP;
  1281. if (serviceActivityCount > 0) {
  1282. return PostbackThreadActionOnEmpty.WAIT;
  1283. }
  1284. if (state == BroadcastState.PAUSING) {
  1285. return setState(BroadcastState.PAUSED, reason, stateErrorText).stateChangeStatus == StateChangeStatus.SUCCESS?
  1286. PostbackThreadActionOnEmpty.CONTINUE : PostbackThreadActionOnEmpty.WAIT;
  1287. }
  1288. if (state == BroadcastState.CANCELING) {
  1289. return setState(BroadcastState.CANCELED, reason, stateErrorText).stateChangeStatus == StateChangeStatus.SUCCESS?
  1290. PostbackThreadActionOnEmpty.CONTINUE : PostbackThreadActionOnEmpty.STOP;
  1291. }
  1292. else if (state == BroadcastState.ABORTING) {
  1293. return setState(BroadcastState.ABORTED, reason, stateErrorText).stateChangeStatus == StateChangeStatus.SUCCESS?
  1294. PostbackThreadActionOnEmpty.CONTINUE : PostbackThreadActionOnEmpty.STOP;
  1295. }
  1296. else if (state == BroadcastState.COMPLETED && scheduledJobs == 0) {
  1297. return setState(BroadcastState.ALLDONE).stateChangeStatus == StateChangeStatus.SUCCESS?
  1298. PostbackThreadActionOnEmpty.CONTINUE : PostbackThreadActionOnEmpty.STOP;
  1299. }
  1300. else {
  1301. return PostbackThreadActionOnEmpty.WAIT;
  1302. }
  1303. }
  1304. /**
  1305. * @return null or configuration in XML
  1306. */
  1307. public String getConfigXML()
  1308. {
  1309. StringBuffer configBuf = new StringBuffer();
  1310. configBuf.append("<broadcast_configuration broadcast_id='" + broadcastId + "'>");
  1311. configBuf.append("<" + DAILY_STOP_KEY + ">" + daily_stop + "</" + DAILY_STOP_KEY + ">");
  1312. configBuf.append("<" + DAILY_START_KEY + ">" + daily_start + "</" + DAILY_START_KEY + ">");
  1313. configBuf.append("</broadcast_configuration>");
  1314. return configBuf.toString();
  1315. }
  1316. public void configure(JSONObject configuration) throws Exception {
  1317. boolean timeChanged = false;
  1318. for (String key : new String[] {DAILY_STOP_KEY, DAILY_START_KEY}) {
  1319. String value = (String)configuration.get(key);
  1320. if (setOperatingHours(key, value)) {
  1321. timeChanged = true;
  1322. }
  1323. }
  1324. if (timeChanged) enforceOperationHours();
  1325. }
  1326. /**
  1327. * YML: At this time, we only enforce pause action when a broadcast is
  1328. * outside its operating hours. The current design is not satisfactory and needs
  1329. * a better solution.
  1330. *
  1331. * We are not automatically resuming a paused broadcast because the difference
  1332. * between intention of an operator-initiated pause and
  1333. * that of a clock pause needs clarification in their operation paradigm.
  1334. * Question is when or if we allow a operator-initiated pause be resumed
  1335. * when someone changes the operating hours of a broadcast in such a way
  1336. * that the broadcast is at once within its operasting hours. it may be
  1337. * be counter to the intention of the original operator.
  1338. *
  1339. * On the other hand, if that places the broadcast outside it operating hours,
  1340. * it is safer to immediately pause it.
  1341. *
  1342. * To add clarity, we may need to separate the PAUSE state into OPERATOR_PAUSE and CLOCK_PAUSE,
  1343. * and similarly PAUING state.
  1344. */
  1345. void enforceOperationHours() {
  1346. if (state == BroadcastState.ABORTED) return;
  1347. if (withinOperatingHours()) {
  1348. // resume("clock", null);
  1349. } else {
  1350. pause("clock", null);
  1351. }
  1352. }
  1353. /**
  1354. * Sets timeParam to value
  1355. * @param timeParam
  1356. * @param value
  1357. * @return false if no change
  1358. */
  1359. private boolean setOperatingHours(String timeParam, String value) {
  1360. if (value == null || value.trim().length() == 0) return false;
  1361. String timeOfDay = CommEngine.checkTimeOfDay(value);
  1362. if (timeOfDay == null) throw new RuntimeException(String.format("Invalid value for %s: %s", timeParam, value));
  1363. switch (timeParam) {
  1364. case DAILY_STOP_KEY:
  1365. if (timeOfDay.equals(daily_stop)) return false;
  1366. daily_stop = timeOfDay;
  1367. return true;
  1368. case DAILY_START_KEY:
  1369. if (timeOfDay.equals(daily_start)) return false;
  1370. daily_start = timeOfDay;
  1371. return true;
  1372. default:
  1373. throw new RuntimeException("Unknown parameter name: " + timeParam);
  1374. }
  1375. }
  1376. @SuppressWarnings("unchecked")
  1377. public JSONObject getConfigJSON() {
  1378. JSONObject dataMap = new JSONObject();
  1379. dataMap.put(DAILY_START_KEY, daily_start);
  1380. dataMap.put(DAILY_STOP_KEY, daily_stop);
  1381. childAddConfigJSON(dataMap);
  1382. return dataMap;
  1383. }
  1384. public void logCSV(String[] data)
  1385. {
  1386. StringWriter writer = new StringWriter();
  1387. @SuppressWarnings("resource")
  1388. CSVWriter csvWriter = new CSVWriter(writer);
  1389. csvWriter.writeNext(data);
  1390. csvLogger.info(writer);
  1391. }
  1392. protected void childAddConfigJSON(JSONObject dataMap) {
  1393. }
  1394. }