Imported from dev1.link2tek.net CommEngine.git
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1528 lines
49 KiB

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