| @@ -0,0 +1,3 @@ | |||
| Manifest-Version: 1.0 | |||
| Class-Path: | |||
| @@ -0,0 +1,893 @@ | |||
| package altk.comm.engine; | |||
| import java.util.ArrayList; | |||
| import java.util.Arrays; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.Queue; | |||
| import java.util.concurrent.Executors; | |||
| import java.util.concurrent.LinkedBlockingQueue; | |||
| import java.util.concurrent.ScheduledExecutorService; | |||
| import java.util.concurrent.ScheduledFuture; | |||
| import java.util.concurrent.TimeUnit; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import org.apache.log4j.Logger; | |||
| import org.apache.log4j.NDC; | |||
| import altk.comm.engine.exception.BroadcastException; | |||
| import altk.comm.engine.exception.EngineException; | |||
| import altk.comm.engine.postback.PostBack; | |||
| /** | |||
| * Broadcast class absorbs what was formerly known as Dispatcher class. | |||
| * | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| public abstract class Broadcast | |||
| { | |||
| private static final int SCHEDULER_THREAD_POOL_SIZE = 5; | |||
| public final String broadcastType; | |||
| private String broadcastId; | |||
| /** | |||
| * Set when reading request XML, but never used. | |||
| */ | |||
| private String launchRecordId; | |||
| // protected XPath xpathEngine; | |||
| protected String postBackURL; | |||
| private PostBack postBack; | |||
| public long expireTime; | |||
| static Logger myLogger = Logger.getLogger(Broadcast.class); | |||
| /** | |||
| * This queue is designed for only one server | |||
| */ | |||
| private Queue<Job> readyQueue; | |||
| private List<Thread> serviceThreadPool; | |||
| private Object resumeFlag; // Semaphore for dispatcher threads to resume. | |||
| protected List<Recipient> recipientList; | |||
| private int remainingJobs; | |||
| private ScheduledExecutorService scheduler; | |||
| public static enum BroadcastState | |||
| { | |||
| INSTALLING, | |||
| RUNNING, | |||
| HALTING, | |||
| HALTED, | |||
| CANCELING, | |||
| CANCELED, // Final state | |||
| PURGED, // Final state | |||
| ABORTED, // final state | |||
| COMPLETED // Final state | |||
| } | |||
| public enum StateChangeStatus | |||
| { | |||
| SUCCESS, | |||
| NO_CHANGE, | |||
| FORBIDDEN | |||
| } | |||
| /** | |||
| * When a Broadcast is first created, its state is INSTALLING, during which | |||
| * time, the broadcast request XML is digested. After this period, the | |||
| * state of the broadcast enters into RUNNING, and it begins with adding | |||
| * all VoiceJobs in the request to the Dispatcher's queues. During this period | |||
| * calls are dispatched, setup, and terminate. When the last call is terminated, | |||
| * the broadcast enters into the COMPLETED state. | |||
| * | |||
| * Transitions from INSTALLING to RUNNING, and from RUNNING to COMPLETED happen | |||
| * automatically without any external influence. | |||
| * | |||
| * At any time, the broadcast may be canceled by user action, which causes | |||
| * the broadcast to transition from the RUNNING state into the CANCELED state. | |||
| * | |||
| * Since the RUNNING state may go to the COMLETED or to the CANCELED state, | |||
| * each due to a different thread, there needs to be a mutex to guarantee | |||
| * state and data integrity. | |||
| * | |||
| * A INSTALLING or RUNNING broadcast may be paused by user action, stopping the | |||
| * Dispatcher from making new calls and causing the broadcast to go to | |||
| * the HALTED state. Certain error conditions result in the HALTED state. | |||
| * | |||
| * User may order a broadcast to be removed entirely with a purge command, | |||
| * causing the broadcast to go to the PURGED state. This state is there so | |||
| * objects, that has reference to a broadcast which has been purged, know | |||
| * what to do in that situation. | |||
| * | |||
| * We need the pause operation to set a RUNNING machine to be in the PAUSING state, | |||
| * allow ongoing jobs to proceed to normal termination, | |||
| * whereas no new jobs are started. State goes from PAUSING to HALTED. | |||
| * | |||
| * Cancel-nice operation is pause, followed by the automatic transition | |||
| * from HALTED to CANCELED. | |||
| * | |||
| * Cancel operation forces all jobs to abort, and the state transitions to CANCELED | |||
| * immediately. | |||
| * | |||
| * Because the Dispatcher and Broadcast is one-to-one, and because they access each other's | |||
| * data, Dispatcher should be combined into Broadcast. | |||
| * | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| static Map<BroadcastState, List<BroadcastState>> toStates; | |||
| static | |||
| { | |||
| // Initialize legal transitions of state machine. | |||
| // For each state, define a list of legal states that this state can transition to | |||
| toStates = new HashMap<BroadcastState, List<BroadcastState>>(); | |||
| // Transitions from INSTALLING | |||
| toStates.put(BroadcastState.INSTALLING, Arrays.asList( | |||
| BroadcastState.RUNNING, // Normal transition | |||
| BroadcastState.CANCELING, // User action | |||
| BroadcastState.CANCELED, // User action | |||
| BroadcastState.HALTING, // User action | |||
| BroadcastState.HALTED, // User action | |||
| BroadcastState.PURGED, // User action | |||
| BroadcastState.ABORTED, // TTS error | |||
| BroadcastState.COMPLETED // When recipient list is empty | |||
| )); | |||
| // Transitions from RUNNING | |||
| toStates.put(BroadcastState.RUNNING, Arrays.asList( | |||
| BroadcastState.CANCELING, // User action | |||
| BroadcastState.CANCELED, // User action | |||
| BroadcastState.HALTING, // User action | |||
| BroadcastState.HALTED, // User action | |||
| BroadcastState.PURGED, // User action | |||
| BroadcastState.ABORTED, // Service provider irrecoverable error | |||
| BroadcastState.COMPLETED // Natural transition, if all ongoing calls complete and no more calls in Dispatcher queues. | |||
| )); | |||
| // Transitions from CANCELING | |||
| toStates.put(BroadcastState.CANCELING, Arrays.asList( | |||
| BroadcastState.CANCELED, // User action | |||
| BroadcastState.PURGED, // User action | |||
| BroadcastState.COMPLETED // Natural transition, if all ongoing calls complete and no more calls in Dispatcher queues. | |||
| )); | |||
| // Transitions from HALTING | |||
| toStates.put(BroadcastState.HALTING, Arrays.asList( | |||
| BroadcastState.RUNNING, // User action | |||
| BroadcastState.CANCELED, // User action | |||
| BroadcastState.HALTED, | |||
| BroadcastState.PURGED, // User action | |||
| BroadcastState.COMPLETED // Natural transition, if all ongoing jobs complete and no more calls in Dispatcher queues. | |||
| )); | |||
| // Transitions from HALTED | |||
| toStates.put(BroadcastState.HALTED, Arrays.asList( | |||
| BroadcastState.RUNNING, // User action | |||
| BroadcastState.CANCELED, // User action | |||
| BroadcastState.CANCELING, // User action | |||
| BroadcastState.PURGED, // User action | |||
| BroadcastState.COMPLETED // Natural transition, if all ongoing jobs complete and no more calls in Dispatcher queues. | |||
| )); | |||
| } | |||
| public static class StateChangeResult | |||
| { | |||
| public StateChangeStatus stateChangeStatus; | |||
| public BroadcastState currentState; | |||
| public BroadcastState previousState; | |||
| public StateChangeResult(StateChangeStatus stateChangeStatus, | |||
| BroadcastState currentState, BroadcastState previousState) | |||
| { | |||
| this.stateChangeStatus = stateChangeStatus; | |||
| this.currentState = currentState; | |||
| this.previousState = previousState; | |||
| } | |||
| } | |||
| private BroadcastState state = BroadcastState.INSTALLING; | |||
| String haltReason; | |||
| String stateErrorText; | |||
| public long changeStateTime; | |||
| protected class Service extends Thread | |||
| { | |||
| Object serviceProvider; | |||
| protected Service(String name) throws BroadcastException | |||
| { | |||
| serviceProvider = getInitializedServiceProvider(); | |||
| setName(name); | |||
| } | |||
| public void run() | |||
| { | |||
| NDC.push(getName()); | |||
| for (;;) | |||
| { | |||
| if (threadsShouldStop()) | |||
| { | |||
| closeServiceProvider(serviceProvider); | |||
| return; | |||
| } | |||
| synchronized (resumeFlag) | |||
| { | |||
| if (threadsShouldPause()) | |||
| { | |||
| try | |||
| { | |||
| resumeFlag.wait(); | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| myLogger.warn("Dispatcher thread interrupted while waiting to resume"); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| List<Job> batch = null; | |||
| /** | |||
| * Includes allocation from capacity. Only returns when the required allocation | |||
| * is obtained. Example, RTP port allocation, limit due to total number of allowable calls. | |||
| */ | |||
| ServicePrerequisites prerequisites = null; | |||
| synchronized(readyQueue) | |||
| { | |||
| // get a batch of jobs | |||
| Job job = readyQueue.peek(); | |||
| if (job == null) | |||
| try | |||
| { | |||
| readyQueue.wait(); | |||
| continue; | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| return; | |||
| } | |||
| prerequisites = secureServicePrerequisites(); | |||
| if (threadsShouldStop() || threadsShouldPause()) | |||
| { | |||
| returnPrerequisites(prerequisites); | |||
| continue; | |||
| } | |||
| // Now that we can go ahead with this job, let us remove this from queue | |||
| readyQueue.poll(); | |||
| batch = new ArrayList<Job>(); | |||
| batch.add(job); | |||
| // We we are to get a batch of more than one, let us fill in the rest. | |||
| for (int i = 1; i < getJobBatchSize(); i++) | |||
| { | |||
| job = readyQueue.poll(); | |||
| if (job == null) break; | |||
| batch.add(job); | |||
| } | |||
| } | |||
| if (batch != null && batch.size() > 0) | |||
| { | |||
| // Mark start time | |||
| long now = System.currentTimeMillis(); | |||
| for (Job job : batch) | |||
| { | |||
| job.startTime = now; | |||
| } | |||
| // Service the jobs | |||
| try | |||
| { | |||
| processJobs(batch, serviceProvider, prerequisites); | |||
| } | |||
| catch (EngineException e) | |||
| { | |||
| terminate(BroadcastState.ABORTED, e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| protected Broadcast(String broadcastType) | |||
| { | |||
| this.broadcastType = broadcastType; | |||
| readyQueue = new LinkedBlockingQueue<Job>(); | |||
| serviceThreadPool = new ArrayList<Thread>(); | |||
| recipientList = new ArrayList<Recipient>(); | |||
| scheduler = Executors.newScheduledThreadPool(SCHEDULER_THREAD_POOL_SIZE); | |||
| resumeFlag = new Object(); | |||
| } | |||
| protected abstract void returnPrerequisites(ServicePrerequisites prerequisites); | |||
| /** | |||
| * Creates and initializes a service provider, to be used by only one service thread. | |||
| * If service provider is not thread-specific, then this method may return null, and | |||
| * a common service provider is created outside of this method. | |||
| * | |||
| * @return service provider as a class Object instance. | |||
| * @throws BroadcastException | |||
| */ | |||
| protected abstract Object getInitializedServiceProvider() throws BroadcastException; | |||
| /** | |||
| * Obtains the required components to support a service; e.g. RTP port, or a place | |||
| * in maximum total number of calls. Does not return till the reequired prerequisites are obtained. | |||
| * @return null, if no prerequisite is required, as in the case of email and sms engines. | |||
| */ | |||
| abstract protected ServicePrerequisites secureServicePrerequisites(); | |||
| abstract public void closeServiceProvider(Object serviceProvider); | |||
| /** | |||
| * Makes a state transition to the given newState if the transition from | |||
| * the current state is legal. | |||
| * @param newState | |||
| * @return StateChangeResult | |||
| */ | |||
| public StateChangeResult setState(BroadcastState newState) | |||
| { | |||
| return setState(newState, null, null); | |||
| } | |||
| /** | |||
| * Makes a state transition to the given newState if the transition from | |||
| * the current state is legal. | |||
| * @param newState | |||
| * @return StateChangeResult | |||
| */ | |||
| public StateChangeResult setState(BroadcastState newState, | |||
| String haltReason, String stateErrorText) | |||
| { | |||
| boolean isLegal; | |||
| BroadcastState prev = null; | |||
| synchronized (this) | |||
| { | |||
| if (state == newState) return new StateChangeResult(StateChangeStatus.NO_CHANGE, state, null); | |||
| List<BroadcastState> to = toStates.get(state); | |||
| isLegal = (to == null? false : to.contains(newState)); | |||
| prev = state; | |||
| if (isLegal) | |||
| { | |||
| state = newState; | |||
| changeStateTime = System.currentTimeMillis(); | |||
| } | |||
| } | |||
| if (isLegal) | |||
| { | |||
| this.haltReason = haltReason; | |||
| this.stateErrorText = stateErrorText; | |||
| CommonLogger.activity.info(String.format("Broadcast %s: State transitioned from %s to %s", broadcastId, prev, state)); | |||
| if (postBack != null) | |||
| { | |||
| postBack.queueReport(mkStatusReport()); | |||
| } | |||
| return new StateChangeResult(StateChangeStatus.SUCCESS, newState, prev); | |||
| } | |||
| else | |||
| { | |||
| myLogger.warn(String.format("Broadcast %s: Transition from %s to %s forbidden", broadcastId, prev, newState)); | |||
| return new StateChangeResult(StateChangeStatus.FORBIDDEN, prev, null); | |||
| } | |||
| } | |||
| protected void setBroadcastId(String broadcastId) | |||
| { | |||
| if (broadcastId == null) | |||
| throw new IllegalArgumentException( | |||
| "Argument broadcastId in Broadcast.setBroadcastId method cannot be null"); | |||
| if (this.broadcastId != null) | |||
| throw new IllegalStateException( | |||
| "Broadcast.setBroadcastId method cannot be invoked more than once for a Broadcast"); | |||
| this.broadcastId = broadcastId; | |||
| } | |||
| protected void setLaunchRecordId(String launchRecordId) | |||
| { | |||
| if (launchRecordId == null) | |||
| throw new IllegalArgumentException( | |||
| "Argument launchRecordId in Broadcast.setLaunchRecordId method cannot be null"); | |||
| if (this.launchRecordId != null) | |||
| throw new IllegalStateException( | |||
| "Broadcast.setLaunchRecordId method cannot be invoked more than once for a Broadcast"); | |||
| this.launchRecordId = launchRecordId; | |||
| } | |||
| public String getBroadcastId() | |||
| { | |||
| return broadcastId; | |||
| } | |||
| public String getLaunchRecordId() | |||
| { | |||
| return launchRecordId; | |||
| } | |||
| public String getResponseXML(BroadcastException e) | |||
| { | |||
| String tagName = broadcastType + "_response"; | |||
| StringBuffer responseXML = new StringBuffer("<" + tagName); | |||
| if (broadcastId != null && broadcastId.length() > 0) | |||
| { | |||
| responseXML.append(" broadcast_id=\""); | |||
| responseXML.append(broadcastId); | |||
| responseXML.append("\""); | |||
| } | |||
| responseXML.append(" accepted='"); | |||
| responseXML.append(e != null || getState() == BroadcastState.COMPLETED ? "FALSE" : "TRUE"); | |||
| responseXML.append("'"); | |||
| if (e == null) | |||
| { | |||
| responseXML.append('>'); | |||
| } | |||
| else | |||
| { | |||
| if (e.errorCode != null) | |||
| { | |||
| responseXML.append(" error='"); | |||
| responseXML.append(e.errorCode.toString()); | |||
| responseXML.append("'"); | |||
| } | |||
| responseXML.append('>'); | |||
| if (e.errorText != null) | |||
| { | |||
| responseXML.append("<error_text>"); | |||
| responseXML.append(e.errorText); | |||
| responseXML.append("</error_text>"); | |||
| } | |||
| } | |||
| responseXML.append("</" + tagName + '>'); | |||
| return responseXML.toString(); | |||
| } | |||
| public String getPostBackURL() | |||
| { | |||
| return postBackURL; | |||
| } | |||
| protected String mkResponseXML(String errorCode, String errorText) | |||
| { | |||
| String tagName = broadcastType + "_response"; | |||
| StringBuffer responseXML = new StringBuffer("<" + tagName); | |||
| String broadcastId = getBroadcastId(); | |||
| if (broadcastId != null && broadcastId.length() > 0) | |||
| { | |||
| responseXML.append(" broadcast_id=\""); | |||
| responseXML.append(broadcastId); | |||
| responseXML.append("\""); | |||
| } | |||
| responseXML.append(" accepted='"); | |||
| responseXML.append(errorCode == null ? "TRUE" : "FALSE"); | |||
| responseXML.append("'"); | |||
| if (errorCode == null) | |||
| { | |||
| responseXML.append('>'); | |||
| } | |||
| else | |||
| { | |||
| responseXML.append(" error='"); | |||
| responseXML.append(errorCode); | |||
| responseXML.append("'"); | |||
| responseXML.append('>'); | |||
| if (errorText != null) | |||
| { | |||
| responseXML.append("<error_text>"); | |||
| responseXML.append(errorText.replaceAll("\\&", "&") | |||
| .replaceAll("<", "<")); | |||
| responseXML.append("</error_text>"); | |||
| } | |||
| } | |||
| responseXML.append("</" + tagName + '>'); | |||
| return responseXML.toString(); | |||
| } | |||
| private boolean stateIsFinal(BroadcastState state) | |||
| { | |||
| return state == BroadcastState.ABORTED || state == BroadcastState.CANCELED | |||
| || state == BroadcastState.COMPLETED || state == BroadcastState.PURGED; | |||
| } | |||
| /** | |||
| * If finalState is final, then this state is set, and dispatcher threads are stopped. | |||
| * Overriding implementation may release all other resources, like timers. | |||
| * @param finalState | |||
| */ | |||
| public void terminate(BroadcastState finalState) | |||
| { | |||
| terminate(finalState, null); | |||
| } | |||
| /** | |||
| * If finalState is final, then this state is set, and dispatcher threads are stopped. | |||
| * Overriding implementation may release all other resources, like timers. | |||
| * @param finalState | |||
| */ | |||
| public void terminate(BroadcastState finalState, String reason) | |||
| { | |||
| if (!stateIsFinal(finalState)) throw new IllegalArgumentException("Argument finalState " + finalState + " in Broadcast.terminate method is not final"); | |||
| setState(finalState, reason, null); | |||
| // Wake up all dispatcher threads waiting on readyQueue so they will all stop | |||
| synchronized(readyQueue) | |||
| { | |||
| readyQueue.notifyAll(); | |||
| } | |||
| // Wake up all sleeping dispatcher threads for same reason. | |||
| for(Thread t : serviceThreadPool) | |||
| { | |||
| try | |||
| { | |||
| t.interrupt(); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| myLogger.warn("Interrupted while waiting for Thread " + t.getName() + " to terminate"); | |||
| } | |||
| } | |||
| // Quiesce scheduler, and terminate it. | |||
| scheduler.shutdownNow(); | |||
| } | |||
| /** | |||
| * Creates status report. | |||
| * @return status report in XML. | |||
| */ | |||
| protected String mkStatusReport() | |||
| { | |||
| StringBuffer statusBf = new StringBuffer(); | |||
| String topLevelTag = broadcastType; | |||
| statusBf.append("<" + topLevelTag + " broadcast_id='" + getBroadcastId() | |||
| + "' recipient_count='" + recipientList.size() + "'"); | |||
| if (launchRecordId != null) | |||
| { | |||
| statusBf.append(" launch_record_id='" + launchRecordId + "'"); | |||
| } | |||
| BroadcastState broadcastState = getState(); | |||
| statusBf.append(">\r\n<state>" + broadcastState + "</state>\r\n"); | |||
| if (broadcastState == BroadcastState.HALTED | |||
| || broadcastState == BroadcastState.ABORTED) | |||
| { | |||
| if (haltReason != null) | |||
| { | |||
| statusBf.append("<reason>" + haltReason | |||
| + "</reason>\r\n"); | |||
| } | |||
| if (stateErrorText != null) | |||
| { | |||
| statusBf.append("<error_text>" + stateErrorText | |||
| + "</error_text>"); | |||
| } | |||
| } | |||
| statusBf.append("<remaining_jobs total='" + remainingJobs + "'"); | |||
| int activeCount = getActiveCount(); | |||
| if (activeCount > -1) statusBf.append(" active='" + activeCount + "'"); | |||
| statusBf.append("></remaining_jobs></" + topLevelTag + ">\r\n"); | |||
| String statusReport = statusBf.toString(); | |||
| return statusReport; | |||
| } | |||
| protected void onExpire() | |||
| { | |||
| } | |||
| protected void setExpireTime(long expireTime) | |||
| { | |||
| this.expireTime = expireTime; | |||
| } | |||
| public long getExpireTime() | |||
| { | |||
| return expireTime; | |||
| } | |||
| /** | |||
| * | |||
| * @return number of active jobs. -1 if there is no concept of being active. | |||
| */ | |||
| private int getActiveCount() | |||
| { | |||
| return remainingJobs - readyQueue.size(); | |||
| } | |||
| /** | |||
| * Parses broadcastId and return if notInService is true. | |||
| * Otherwise, continue parsing postBackUrl, expireTime, recipientList, | |||
| * and implementation-specific data from request. | |||
| * Avoid throwing an exception before parsing and setting broadcastId. | |||
| * @param notInService | |||
| * @throws EngineException | |||
| */ | |||
| protected abstract void decode(HttpServletRequest request, boolean notInService) | |||
| throws EngineException; | |||
| /** | |||
| * Remembers postBack, and | |||
| * Creates thread pool of size dictated by broadcast, which determines the size based | |||
| * on the chosen service provider. | |||
| * | |||
| * Overriding implementation must invoke this method at the end, and process information | |||
| * contained in the broadcast, in preparation for the invocation of the process | |||
| * method. | |||
| * | |||
| * If there is no error, the overriding implementation must return this base method. | |||
| * | |||
| * @param commEngine | |||
| * | |||
| * @throws BroadcastException | |||
| */ | |||
| protected final void init(PostBack postBack) | |||
| { | |||
| // Remember postBack | |||
| this.postBack = postBack; | |||
| for (Recipient recipient : recipientList) | |||
| { | |||
| readyQueue.add(mkJob(recipient)); | |||
| } | |||
| remainingJobs = readyQueue.size(); | |||
| } | |||
| protected abstract void initSync(EngineResources resources) throws BroadcastException; | |||
| protected Job mkJob(Recipient recipient) | |||
| { | |||
| return new Job(recipient); | |||
| } | |||
| /** | |||
| * Overriding implementation performs time consuming initialization, after returning | |||
| * POST http status indicating accepting broadcast for processing. | |||
| * | |||
| * @throws BroadcastException | |||
| */ | |||
| protected void initAsync() throws BroadcastException | |||
| { | |||
| // Do nothing in base class. | |||
| } | |||
| public abstract int getServiceThreadPoolSize(); | |||
| public String getId() | |||
| { | |||
| return broadcastId; | |||
| } | |||
| /** | |||
| * Sets the stateMachine to CANCEL | |||
| */ | |||
| protected void cancel() | |||
| { | |||
| if (this.getActiveCount() == 0) setState(BroadcastState.CANCELED); | |||
| // Sets state to CANCELING, which is monitored by its Broadcast.Service threads. | |||
| else setState(BroadcastState.CANCELING); | |||
| synchronized(resumeFlag) | |||
| { | |||
| resumeFlag.notifyAll(); | |||
| } | |||
| } | |||
| protected void pause() | |||
| { | |||
| // Sets state to HALTED, which is monitored by Broadcast.Service threads. | |||
| setState(BroadcastState.HALTING); | |||
| } | |||
| protected void resume() | |||
| { | |||
| synchronized (resumeFlag) | |||
| { | |||
| if (threadsShouldPause()) | |||
| { | |||
| setState(BroadcastState.RUNNING); | |||
| resumeFlag.notifyAll(); | |||
| } | |||
| } | |||
| } | |||
| abstract protected JobReport mkJobReport(Job job); | |||
| public void addJob(Job job) | |||
| { | |||
| synchronized(readyQueue) | |||
| { | |||
| readyQueue.add(job); | |||
| readyQueue.notifyAll(); | |||
| } | |||
| } | |||
| public void startProcessing() throws BroadcastException | |||
| { | |||
| // Create dispatcher thread pool | |||
| int threadPoolSize = getServiceThreadPoolSize(); | |||
| for (int i = 0; i < threadPoolSize; i++) | |||
| { | |||
| String threadName = broadcastId + "_service_thread_" + i; | |||
| Service serviceThread = new Service(threadName); | |||
| serviceThreadPool.add(serviceThread); | |||
| } | |||
| setState(BroadcastState.RUNNING); | |||
| // Start the dispatcher threads | |||
| for (Thread thread : serviceThreadPool) | |||
| { | |||
| thread.start(); | |||
| } | |||
| } | |||
| private boolean threadsShouldStop() | |||
| { | |||
| BroadcastState state = getState(); | |||
| return state == BroadcastState.CANCELING || stateIsFinal(state); | |||
| } | |||
| private boolean threadsShouldPause() | |||
| { | |||
| BroadcastState state = getState(); | |||
| return state == BroadcastState.HALTED || state == BroadcastState.HALTING; | |||
| } | |||
| /* | |||
| @Override | |||
| public void run() | |||
| { | |||
| for (;;) | |||
| { | |||
| if (threadsShouldStop()) return; | |||
| if (threadsShouldPause()) | |||
| { | |||
| try | |||
| { | |||
| resumeFlag.wait(); | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| myLogger.warn("Dispatcher thread interrupted while waiting to resume"); | |||
| return; | |||
| } | |||
| } | |||
| List<Job> batch = null; | |||
| synchronized(readyQueue) | |||
| { | |||
| // get a batch of jobs | |||
| Job job = readyQueue.poll(); | |||
| if (job == null) | |||
| try | |||
| { | |||
| readyQueue.wait(); | |||
| continue; | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| return; | |||
| } | |||
| batch = new ArrayList<Job>(); | |||
| batch.add(job); | |||
| for (int i = 1; i < getJobBatchSize(); i++) | |||
| { | |||
| job = readyQueue.poll(); | |||
| if (job == null) break; | |||
| batch.add(job); | |||
| } | |||
| } | |||
| if (batch != null) | |||
| { | |||
| try | |||
| { | |||
| processJobs(batch); | |||
| } | |||
| catch (EngineException e) | |||
| { | |||
| terminate(BroadcastState.ABORTED, e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| */ | |||
| /** | |||
| * job status is reported back to this broadcast, via the logAndQueueForPostBack method. | |||
| * @param batch | |||
| * @param prerequisites | |||
| */ | |||
| abstract protected void processJobs(List<Job> batch, Object serviceProvider, ServicePrerequisites prerequisites) | |||
| throws EngineException; | |||
| /** | |||
| * Size of a batch of jobs to be processed together. For email, this may be more than 1, | |||
| * and this method should be overridden. | |||
| * @return size of a batch of jobs to be processed together. | |||
| */ | |||
| protected int getJobBatchSize() | |||
| { | |||
| return 1; | |||
| } | |||
| /** | |||
| * Sets jobStatus in job, and post job report. | |||
| * If no rescheduling, then decrement number of remainingJobs, | |||
| * @param job | |||
| * @param jobStatus | |||
| * @param errorText | |||
| */ | |||
| public void postJobStatus(Job job) | |||
| { | |||
| if (postBack != null) | |||
| { | |||
| JobReport report = mkJobReport(job); | |||
| postBack.queueReport(report.toString()); | |||
| } | |||
| if (job.jobStatus.isTerminal()) | |||
| { | |||
| remainingJobs--; | |||
| if (remainingJobs == 0) | |||
| { | |||
| terminate(BroadcastState.COMPLETED); | |||
| } | |||
| else if (getActiveCount() == 0) | |||
| { | |||
| if (state == BroadcastState.CANCELING) setState(BroadcastState.CANCELED); | |||
| else if (state == BroadcastState.HALTING) setState(BroadcastState.HALTED); | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Sets jobStatus in job, and post job report. | |||
| * If no rescheduling, then decrement number of remainingJobs, | |||
| * @param job | |||
| * @param jobStatus | |||
| * @param errorText | |||
| * @param reschedule - reschedule time in milliseconds (-1 means do not reschedule). | |||
| */ | |||
| protected void postJobStatus(Job job, long rescheduleTimeMS) | |||
| { | |||
| postJobStatus(job); | |||
| if (rescheduleTimeMS == 0) | |||
| { | |||
| addJob(job); | |||
| } | |||
| else if (rescheduleTimeMS > 0) | |||
| { | |||
| rescheduleJob(job, rescheduleTimeMS); | |||
| } | |||
| } | |||
| public ScheduledFuture<?> rescheduleJob(final Job job, long rescheduleTimeMS) | |||
| { | |||
| Runnable r = new Runnable() { public void run() { addJob(job);}}; | |||
| return scheduler.schedule(r, rescheduleTimeMS, TimeUnit.MILLISECONDS); | |||
| } | |||
| public BroadcastState getState() | |||
| { | |||
| return state; | |||
| } | |||
| } | |||
| @@ -0,0 +1,455 @@ | |||
| package altk.comm.engine; | |||
| import java.io.File; | |||
| import java.io.FileInputStream; | |||
| import java.io.FileNotFoundException; | |||
| import java.io.IOException; | |||
| import java.io.PrintWriter; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| import java.util.Properties; | |||
| import java.util.Vector; | |||
| import java.util.concurrent.Executors; | |||
| import java.util.concurrent.ScheduledExecutorService; | |||
| import java.util.concurrent.TimeUnit; | |||
| import javax.servlet.ServletContext; | |||
| import javax.servlet.http.HttpServlet; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import javax.servlet.http.HttpServletResponse; | |||
| import org.apache.log4j.Logger; | |||
| import altk.comm.engine.Broadcast.BroadcastState; | |||
| import altk.comm.engine.exception.BroadcastError; | |||
| import altk.comm.engine.exception.BroadcastException; | |||
| import altk.comm.engine.exception.PlatformError; | |||
| import altk.comm.engine.exception.PlatformException; | |||
| import altk.comm.engine.postback.PostBack; | |||
| public abstract class CommEngine extends HttpServlet | |||
| { | |||
| /** | |||
| * | |||
| */ | |||
| private static final long serialVersionUID = 6887055442875818654L; | |||
| static final String REQUEST_TOP_ELEMENT_NAME_DEFAULT = "Request"; | |||
| private static final int SCHEDULER_THREAD_POOL_SIZE = 1; | |||
| private static final long DEAD_BROADCAST_VIEWING_PERIOD_DEFAULT = 60; | |||
| /** | |||
| * Maps a broadcastId to a broadcast. | |||
| */ | |||
| private Map<String, Broadcast> broadcasts; | |||
| protected boolean notInService; | |||
| protected Properties config; | |||
| protected Map<String, PostBack> postBackMap; | |||
| protected final String engineName; // e.g. "broadcast_sms", "broadcast_voice" | |||
| private long startupTimestamp; | |||
| // Sequencing naming of broadcast that fails to yield its broadcastId | |||
| private int unknownBroadcastIdNdx = 1; | |||
| private BroadcastException myException; | |||
| /** | |||
| * Used to communicate media-specific platform resources to broadcasts | |||
| */ | |||
| protected EngineResources resources; | |||
| private static Logger myLogger = Logger.getLogger(CommEngine.class); | |||
| private ScheduledExecutorService scheduler; | |||
| private long deadBroadcastViewingMinutes; | |||
| abstract protected Broadcast mkBroadcast(); | |||
| public CommEngine(String engineName) | |||
| { | |||
| this.engineName = engineName; | |||
| broadcasts = new HashMap<String, Broadcast>(); | |||
| startupTimestamp = System.currentTimeMillis(); | |||
| myException = null; | |||
| } | |||
| /** | |||
| * Invoked by servlet container during initialization of servlet. | |||
| */ | |||
| public final void init() | |||
| { | |||
| myLogger.info("init() invoked"); | |||
| // check init parameters | |||
| ServletContext servletContext = getServletContext(); | |||
| String propertiesFilePath; | |||
| propertiesFilePath = servletContext.getInitParameter(getPropertiesContextName()); | |||
| File propertiesFile = new File(propertiesFilePath); | |||
| CommonLogger.startup.info("Using configuration file " + propertiesFile.getAbsolutePath()); | |||
| config = new Properties(); | |||
| try | |||
| { | |||
| config.load(new FileInputStream(propertiesFile)); | |||
| } | |||
| catch (FileNotFoundException e) | |||
| { | |||
| CommonLogger.alarm.fatal("Properties file " + propertiesFile.getAbsolutePath() + " not found -- abort"); | |||
| notInService = true; | |||
| return; | |||
| } | |||
| catch (IOException e) | |||
| { | |||
| CommonLogger.alarm.fatal("Problem in reading properties file " + propertiesFile.getAbsolutePath() + ": " + e.getMessage()); | |||
| notInService = true; | |||
| return; | |||
| } | |||
| postBackMap = new HashMap<String, PostBack>(); | |||
| // Set up periodic purge of stale broadcasts, based on deadBroadcastViewingMinutes | |||
| String periodStr = config.getProperty("dead_broadcast_viewing_period", | |||
| new Long(DEAD_BROADCAST_VIEWING_PERIOD_DEFAULT).toString()); | |||
| deadBroadcastViewingMinutes = Long.parseLong(periodStr); | |||
| CommonLogger.startup.info(String.format("Dead broadcast viewing period: %d minutes", deadBroadcastViewingMinutes)); | |||
| scheduler = Executors.newScheduledThreadPool(SCHEDULER_THREAD_POOL_SIZE); | |||
| scheduler.scheduleAtFixedRate(new Runnable() { public void run() { purgeStaleBroadcasts();}}, | |||
| deadBroadcastViewingMinutes, deadBroadcastViewingMinutes, TimeUnit.MINUTES); | |||
| initChild(); | |||
| } | |||
| protected void purgeStaleBroadcasts() | |||
| { | |||
| long now = System.currentTimeMillis(); | |||
| for (String id : broadcasts.keySet()) | |||
| { | |||
| if (broadcasts.get(id).changeStateTime - now > deadBroadcastViewingMinutes * 60 * 1000) | |||
| { | |||
| broadcasts.remove(id); | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @return name of parameter in Tomcat context file, specifying properties file | |||
| * for this SMSEngine. | |||
| */ | |||
| protected abstract String getPropertiesContextName(); | |||
| @Override | |||
| protected void doPost(HttpServletRequest request, HttpServletResponse response) | |||
| { | |||
| try | |||
| { | |||
| Broadcast broadcast = mkBroadcast(); | |||
| try | |||
| { | |||
| broadcast.decode(request, notInService); | |||
| if (notInService) | |||
| { | |||
| throw new PlatformException(PlatformError.RUNTIME_ERROR, | |||
| "Not in service"); | |||
| } | |||
| if (broadcast.recipientList.size() == 0) | |||
| { | |||
| CommonLogger.activity.info("Broadcast " + broadcast.getBroadcastId() + ": No recipients"); | |||
| broadcast.setState(BroadcastState.COMPLETED, "No recipients", null); | |||
| return; | |||
| } | |||
| // Determine postBackUrl | |||
| String postBackURL = broadcast.getPostBackURL(); | |||
| PostBack postBack = null; | |||
| if (postBackURL != null) | |||
| { | |||
| postBack = postBackMap.get(postBackURL); | |||
| if (postBack == null) | |||
| { | |||
| postBack = new PostBack(postBackURL, broadcast.broadcastType + "_status"); | |||
| postBackMap.put(postBackURL, postBack); | |||
| } | |||
| } | |||
| broadcast.initSync(resources); | |||
| broadcast.init(postBack); | |||
| if (broadcast.getState() == BroadcastState.COMPLETED) return; | |||
| } | |||
| catch (BroadcastException e) | |||
| { | |||
| myException = e; | |||
| broadcast.setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText); | |||
| CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage()); | |||
| myLogger.error("Broadcast aborted", e); | |||
| return; | |||
| } | |||
| catch (Throwable t) | |||
| { | |||
| // Caught stray unexpected runtime problem | |||
| CommonLogger.alarm.error("Broadcast aborted: " + t); | |||
| myLogger.error("Broadcast aborted", t); | |||
| myException = new BroadcastException(BroadcastError.PLATFORM_ERROR, t.getMessage()); | |||
| broadcast.setState(BroadcastState.ABORTED, myException.errorCodeText, myException.errorText); | |||
| } | |||
| finally | |||
| { | |||
| // Put broadcast in broadcasts map. | |||
| String broadcastId = broadcast.getBroadcastId(); | |||
| if (broadcastId.length() != 0) | |||
| { | |||
| broadcasts.put(broadcastId, broadcast); | |||
| } | |||
| else | |||
| { | |||
| String makeUpId = "Unknown" + unknownBroadcastIdNdx++; | |||
| broadcasts.put(makeUpId, broadcast); | |||
| } | |||
| // Return regular or error response | |||
| String responseXML = broadcast.getResponseXML(myException); | |||
| PrintWriter writer = response.getWriter(); | |||
| writer.write(responseXML); | |||
| writer.close(); | |||
| } | |||
| try | |||
| { | |||
| broadcast.initAsync(); | |||
| broadcast.startProcessing(); | |||
| } | |||
| catch (BroadcastException e) | |||
| { | |||
| broadcast.setState(BroadcastState.ABORTED, e.errorCodeText, e.errorText); | |||
| CommonLogger.alarm.error("Broadcast aborted: " + e.getMessage()); | |||
| myLogger.error("Broadcast aborted", e); | |||
| } | |||
| } | |||
| catch (Throwable t) | |||
| { | |||
| // Caught stray unexpected runtime problem | |||
| CommonLogger.alarm.error("Broadcast aborted: " + t.getMessage()); | |||
| myLogger.error("Broadcast aborted", t); | |||
| } | |||
| } | |||
| /** | |||
| * Functions covered are | |||
| * get=status | |||
| * get=cancel_broadcast&broadcast_id=XXX | |||
| */ | |||
| @Override | |||
| protected void doGet(HttpServletRequest request, HttpServletResponse response) | |||
| { | |||
| PrintWriter out; | |||
| try | |||
| { | |||
| out = response.getWriter(); | |||
| } | |||
| catch (IOException e) | |||
| { | |||
| CommonLogger.alarm.error("Cannot write a reply: " + e); | |||
| return; | |||
| } | |||
| String get = (String)request.getParameter("get"); | |||
| if (get == null) | |||
| { | |||
| // Return http status BAD REQUEST | |||
| int httpStatus = HttpServletResponse.SC_BAD_REQUEST; | |||
| try | |||
| { | |||
| response.sendError(httpStatus); | |||
| } | |||
| catch (IOException e) | |||
| { | |||
| myLogger.warn("Unnable to return HTTP error code " + httpStatus); | |||
| } | |||
| return; | |||
| } | |||
| if (get.equalsIgnoreCase("status")) | |||
| { | |||
| getStatus(request, out); | |||
| } | |||
| else if (get.equalsIgnoreCase("cancel_broadcast")) | |||
| { | |||
| cancelBroadcast(request, out); | |||
| } | |||
| else if (get.equalsIgnoreCase("pause_broadcast")) | |||
| { | |||
| pauseBroadcast(request, out); | |||
| } | |||
| else if (get.equalsIgnoreCase("resume_broadcast")) | |||
| { | |||
| resumeBroadcast(request, out); | |||
| } | |||
| else | |||
| { | |||
| out.write(get + " not supported"); | |||
| } | |||
| out.close(); | |||
| } | |||
| private void cancelBroadcast(HttpServletRequest request, PrintWriter out) | |||
| { | |||
| // Get broadcastId from request | |||
| String broadcastId = getBroadcastId(request); | |||
| Broadcast broadcast = broadcasts.get(broadcastId); | |||
| if (broadcast == null) | |||
| { | |||
| out.format("Broadcast %s does not exist", broadcastId); | |||
| return; | |||
| } | |||
| broadcast.cancel(); | |||
| } | |||
| protected void pauseBroadcast(HttpServletRequest request, PrintWriter out) | |||
| { | |||
| // Get broadcastId from request | |||
| String broadcastId = getBroadcastId(request); | |||
| Broadcast broadcast = broadcasts.get(broadcastId); | |||
| if (broadcast == null) | |||
| { | |||
| out.format("Broadcast %s does not exist", broadcastId); | |||
| return; | |||
| } | |||
| broadcast.pause(); | |||
| } | |||
| protected void resumeBroadcast(HttpServletRequest request, PrintWriter out) | |||
| { | |||
| // Get broadcastId from request | |||
| String broadcastId = getBroadcastId(request); | |||
| Broadcast broadcast = broadcasts.get(broadcastId); | |||
| if (broadcast == null) | |||
| { | |||
| out.format("Broadcast %s does not exist", broadcastId); | |||
| return; | |||
| } | |||
| broadcast.resume(); | |||
| } | |||
| /** | |||
| * <CallEngine_status> | |||
| * status of each broadcast | |||
| * <calls><total>ttt</total><connected>nnn</connected> | |||
| * </CallEngine_status> | |||
| */ | |||
| private void getStatus(HttpServletRequest request, PrintWriter out) | |||
| { | |||
| String broadcastId = request.getParameter("broadcast_id"); | |||
| if (broadcastId != null) | |||
| { | |||
| broadcastId = broadcastId.trim(); | |||
| if (broadcastId.length() == 0) | |||
| { | |||
| out.write("broadcast_id request parameter cannot be empty"); | |||
| return; | |||
| } | |||
| Broadcast broadcast = broadcasts.get(broadcastId); | |||
| if (broadcast == null) | |||
| { | |||
| out.write("<error>No such broadcast</error>"); | |||
| } | |||
| else | |||
| { | |||
| out.write(broadcast.mkStatusReport()); | |||
| } | |||
| return; | |||
| } | |||
| else | |||
| { | |||
| String tag = engineName + "_status"; | |||
| out.write("<" + tag + ">\r\n"); | |||
| out.write("<startup_time>" + startupTimestamp | |||
| + "</startup_time>\r\n"); | |||
| // First get a copy of broadcasts, to avoid mutex deadlock. | |||
| Vector<Broadcast> broadcastList = new Vector<Broadcast>(); | |||
| synchronized(broadcasts) | |||
| { | |||
| for (String key : broadcasts.keySet()) | |||
| { | |||
| broadcastList.add(broadcasts.get(key)); | |||
| } | |||
| } | |||
| // We have released the lock. | |||
| // Then append status of each broadcast to outBuf. | |||
| for (Broadcast broadcast : broadcastList) | |||
| { | |||
| out.write(broadcast.mkStatusReport()); | |||
| } | |||
| out.write("</" + tag + ">"); | |||
| } | |||
| } | |||
| public void removeBroadcast(String broadcastId) | |||
| { | |||
| CommonLogger.activity.info("Removing broadcast " + broadcastId); | |||
| synchronized(broadcasts) | |||
| { | |||
| broadcasts.remove(broadcastId); | |||
| } | |||
| } | |||
| protected int getRemainingJobCount() | |||
| { | |||
| // TODO | |||
| return 0; | |||
| } | |||
| public boolean notInService() | |||
| { | |||
| return notInService; | |||
| } | |||
| /** | |||
| * Decode http GET request for broadcast_id value | |||
| * @param request | |||
| * @return broadcast_id | |||
| */ | |||
| private String getBroadcastId(HttpServletRequest request) | |||
| { | |||
| return request.getParameter("broadcast_id"); | |||
| } | |||
| /** | |||
| * Invoked by servlet container when servlet is destroyed. | |||
| */ | |||
| public final void destroy() | |||
| { | |||
| System.out.println(engineName + " destroyed"); | |||
| // Kill threads in each PostBack, which is remembered in postBackMap. | |||
| for (PostBack postback : postBackMap.values()) | |||
| { | |||
| postback.terminate(); | |||
| } | |||
| for (Broadcast broadcast : broadcasts.values()) | |||
| { | |||
| broadcast.terminate(BroadcastState.ABORTED, "Platform termination"); | |||
| } | |||
| destroyChild(); | |||
| } | |||
| /** | |||
| * Indirectly invoked by servlet container during servlet initialization. | |||
| */ | |||
| abstract protected void initChild(); | |||
| /** | |||
| * Indirectly invoked by serlet container during destruction of servlet. | |||
| */ | |||
| abstract protected void destroyChild(); | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| package altk.comm.engine; | |||
| import org.apache.log4j.Logger; | |||
| public class CommonLogger | |||
| { | |||
| static public final Logger startup = Logger.getLogger("startup.console"); | |||
| static public final Logger alarm = Logger.getLogger("alarm.console"); | |||
| static public final Logger activity = Logger.getLogger("activity.console"); | |||
| public static final Logger health = Logger.getLogger("health"); | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| package altk.comm.engine; | |||
| /** | |||
| * Base class to transmit engine resources from engine to broadcast. | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| public class EngineResources | |||
| { | |||
| } | |||
| @@ -0,0 +1,85 @@ | |||
| package altk.comm.engine; | |||
| /** | |||
| * Derived classes may add more class attributes, e.g. list of phone numbers, call status. | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| public class Job | |||
| { | |||
| /** | |||
| * | |||
| */ | |||
| static public enum JobStatus | |||
| { | |||
| READY(false), | |||
| TRYING(false), | |||
| GO_NEXT_PHONE(false), | |||
| TRUNK_ERROR(false), | |||
| LONG_DURATION(true), | |||
| SUCCESS(true), | |||
| NO_MORE_RETRY(true), | |||
| RECIPIENT_ADDR_ERROR(true), | |||
| SERVICE_PROVIDER_PROTOCOL_ERROR(true), | |||
| SERVICE_PROVIDER_GENERATED_ERROR(true), | |||
| SERVICE_PROVIDER_HTTP_ERROR(true), | |||
| SERVICE_PROVIDER_CONNECT_ERROR(true), | |||
| SERVICE_PROVIDER_OTHER_ERROR(true), | |||
| CANCELED(true), | |||
| EXPIRED(true), | |||
| MESSAGE_ERROR(true), | |||
| SERVICE_ACCESS_BLOCKED(true), | |||
| PLATFORM_ERROR(true), | |||
| FAILED(true), // for | |||
| UNKNOWN_ERROR(true), | |||
| ABORT(true); | |||
| private boolean isTerminal; | |||
| private JobStatus(boolean isTerminal) | |||
| { | |||
| this.isTerminal = isTerminal; | |||
| } | |||
| public boolean isTerminal() | |||
| { | |||
| return isTerminal; | |||
| } | |||
| } | |||
| public Recipient recipient; | |||
| public long startTime; | |||
| public JobStatus jobStatus; | |||
| public String errorText; | |||
| public Job(Recipient recipient) | |||
| { | |||
| this.recipient = recipient; | |||
| jobStatus = JobStatus.READY; | |||
| } | |||
| public void setStatus(JobStatus jobStatus) | |||
| { | |||
| this.jobStatus = jobStatus; | |||
| } | |||
| public void setStatus(JobStatus jobStatus, String errorText) | |||
| { | |||
| this.jobStatus = jobStatus; | |||
| this.errorText = errorText; | |||
| } | |||
| /** | |||
| * | |||
| * @return error in text form. | |||
| */ | |||
| public String getErrorText() | |||
| { | |||
| return errorText; | |||
| } | |||
| } | |||
| @@ -0,0 +1,91 @@ | |||
| package altk.comm.engine; | |||
| abstract public class JobReport | |||
| { | |||
| private static final String ACTIVITY_RECORD_ID_NAME = "activity_record_id"; | |||
| public final long reportTime; | |||
| public final String broadcastId; | |||
| public final String launchRecordId; | |||
| public final String contactId; | |||
| public final String recordId; // id into table, e.g. call_record, sms_record, etc. | |||
| public final String jobStatus; // Note: not enum | |||
| public final String errorText; | |||
| public long startTime; | |||
| public JobReport(Job job, String broadcastId, String launchRecordId) | |||
| { | |||
| if (broadcastId == null || broadcastId.length() == 0) | |||
| { | |||
| throw new IllegalArgumentException("JobReport given null or empty broadcastId"); | |||
| } | |||
| if (launchRecordId == null || launchRecordId.length() == 0) | |||
| { | |||
| throw new IllegalArgumentException("JobReport given null or empty launchRecordId"); | |||
| } | |||
| this.broadcastId = broadcastId; | |||
| this.launchRecordId = launchRecordId; | |||
| startTime = job.startTime; | |||
| contactId = job.recipient.contact_id; | |||
| recordId = job.recipient.activity_record_id; | |||
| jobStatus = job.jobStatus.toString(); | |||
| errorText = job.errorText; | |||
| reportTime = System.currentTimeMillis(); | |||
| } | |||
| /** | |||
| * @return "email" for example. | |||
| */ | |||
| abstract protected String getXMLRootNodeName(); | |||
| /** | |||
| * @return "email_record_id" for example. | |||
| abstract protected String getActivityRecordIdname(); | |||
| */ | |||
| public String toString() | |||
| { | |||
| StringBuffer xml = new StringBuffer(); | |||
| appendXML(xml); | |||
| return xml.toString(); | |||
| } | |||
| public final StringBuffer appendXML(StringBuffer xml) | |||
| { | |||
| xml.append("<" + getXMLRootNodeName() + " broadcast_id=\"" + broadcastId | |||
| + "\" launch_record_id=\"" + launchRecordId | |||
| + "\" " + getActivityRecordIdName() + "=\"" + recordId | |||
| + "\" contact_id=\"" + contactId | |||
| + "\" recipient_status=\"" + jobStatus + "\" >\r\n"); | |||
| xml.append("<start_time>" + startTime/1000 + "</start_time>\r\n"); | |||
| xml = appendSpecificXML(xml); | |||
| if (errorText != null && errorText.length() > 0) | |||
| { | |||
| xml.append("<error_text>"); | |||
| xml.append(errorText.replaceAll("&", "&").replaceAll("<", "<")); | |||
| xml.append("</error_text>\r\n"); | |||
| } | |||
| xml.append("</" + getXMLRootNodeName() + ">"); | |||
| return xml; | |||
| } | |||
| /** | |||
| * Derived class may override this method to re-define name of activity_record_id, | |||
| * e.g. email_record_id, sms_record_id, etc. | |||
| * @return | |||
| */ | |||
| protected String getActivityRecordIdName() | |||
| { | |||
| return ACTIVITY_RECORD_ID_NAME; | |||
| } | |||
| /** | |||
| * Append data to xml which is specific to the derived class. For example, email | |||
| * address for EmailJobReport. | |||
| * @param xml | |||
| * @return | |||
| */ | |||
| protected abstract StringBuffer appendSpecificXML(StringBuffer xml); | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| package altk.comm.engine; | |||
| import java.util.Map; | |||
| public class Recipient | |||
| { | |||
| /** | |||
| * contact_id in client database | |||
| */ | |||
| public final String contact_id; | |||
| /** | |||
| * E.g. email_record_id, call_record_id, sms_record_id. | |||
| */ | |||
| public final String activity_record_id; | |||
| /** | |||
| * Like names, address, polling places, etc. | |||
| */ | |||
| public final Map<String, String> attributes; | |||
| /** | |||
| * | |||
| * @param contact_id cannot be empty | |||
| * @param activity_record_id cannot be empty | |||
| * @param attributes may be null | |||
| */ | |||
| public Recipient(String contact_id, String activity_record_id, Map<String, String> attributes) | |||
| { | |||
| if (contact_id == null || (contact_id=contact_id.trim()).length() == 0) throw new IllegalArgumentException("empty contact_id given to Recipient"); | |||
| if (activity_record_id == null || (activity_record_id=activity_record_id.trim()).length() == 0) throw new IllegalArgumentException("empty activity_record_id given to Recipient"); | |||
| this.contact_id = contact_id; | |||
| this.activity_record_id = activity_record_id; | |||
| this.attributes = attributes; | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| package altk.comm.engine; | |||
| /** | |||
| * Base class to transmit allocated resources necessary to service a job | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| public class ServicePrerequisites | |||
| { | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| package altk.comm.engine; | |||
| @SuppressWarnings("serial") | |||
| public class ServiceProviderFactoryException extends Exception | |||
| { | |||
| public ServiceProviderFactoryException(String arg0) | |||
| { | |||
| super(arg0); | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| package altk.comm.engine; | |||
| import java.util.Properties; | |||
| public class Util | |||
| { | |||
| static public String getStringParameter(String name, Properties config) | |||
| { | |||
| return getStringParameter(name, config, null); | |||
| } | |||
| static public String getStringParameter(String name, Properties config, StringBuffer errorText) | |||
| { | |||
| String value = config.getProperty(name); | |||
| if (value == null || (value=value.trim()).length() == 0) | |||
| { | |||
| if (errorText == null) return null; | |||
| if (errorText.length() > 0) errorText.append(", "); | |||
| errorText.append("Missing parameter " + name); | |||
| return null; | |||
| } | |||
| return value; | |||
| } | |||
| static public int getIntegerParameter(String name, Properties config) | |||
| { | |||
| return getIntegerParameter(name, config, null); | |||
| } | |||
| static public int getIntegerParameter(String name, Properties config, StringBuffer errorText) | |||
| { | |||
| String value = getStringParameter(name, config, errorText); | |||
| if (value != null) | |||
| { | |||
| try | |||
| { | |||
| return Integer.parseInt(value); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| if (errorText != null) errorText.append("Parameter " + name + " not integer"); | |||
| return 0; | |||
| } | |||
| } | |||
| return 0; | |||
| } | |||
| static public boolean getBooleanParameter(String name, Properties config) | |||
| { | |||
| return getBooleanParameter(name, config, null); | |||
| } | |||
| static public boolean getBooleanParameter(String name, Properties config, StringBuffer errorText) | |||
| { | |||
| String value = getStringParameter(name, config, errorText); | |||
| if (value != null) | |||
| { | |||
| try | |||
| { | |||
| return Boolean.parseBoolean(value); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| errorText.append("Parameter " + name + " not integer"); | |||
| return false; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,416 @@ | |||
| package altk.comm.engine; | |||
| import java.io.ByteArrayInputStream; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| import java.util.Vector; | |||
| import java.util.zip.ZipInputStream; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import javax.xml.parsers.DocumentBuilder; | |||
| import javax.xml.parsers.DocumentBuilderFactory; | |||
| import javax.xml.xpath.XPath; | |||
| import javax.xml.xpath.XPathConstants; | |||
| import javax.xml.xpath.XPathExpressionException; | |||
| import javax.xml.xpath.XPathFactory; | |||
| import org.w3c.dom.Document; | |||
| import org.w3c.dom.Element; | |||
| import org.w3c.dom.NamedNodeMap; | |||
| import org.w3c.dom.Node; | |||
| import org.w3c.dom.NodeList; | |||
| import org.xml.sax.SAXException; | |||
| import altk.comm.engine.exception.BroadcastError; | |||
| import altk.comm.engine.exception.BroadcastException; | |||
| import altk.comm.engine.exception.BroadcastMsgException; | |||
| import altk.comm.engine.exception.EngineException; | |||
| import altk.comm.engine.exception.InternalErrorException; | |||
| import altk.comm.engine.exception.NonExistentException; | |||
| import altk.comm.engine.exception.PlatformError; | |||
| import altk.comm.engine.exception.PlatformException; | |||
| /** | |||
| * Abstract class extending Broadcast by providing an implementation of the decode | |||
| * method to parse HTTP POST input into XML DOM, and extracting the required class | |||
| * attributes broadcast_id, launch_record_id, expire_time, postback and recipientList. | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| public abstract class XMLDOMBroadcast extends Broadcast | |||
| { | |||
| protected XPath xpathEngine; | |||
| protected DocumentBuilder builder; | |||
| protected Element broadcastNode; | |||
| protected XMLDOMBroadcast(String broadcastType) | |||
| { | |||
| super(broadcastType); | |||
| } | |||
| /** | |||
| * Sets up XMLDoc and parses broadcast_id, expire_time, postBackUrl and recipientList. | |||
| * Derived class cannot override this method, which is final. | |||
| * @throws EngineException | |||
| */ | |||
| @Override | |||
| protected final void decode(HttpServletRequest request, boolean notInService) throws EngineException | |||
| { | |||
| try | |||
| { | |||
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |||
| builder = factory.newDocumentBuilder(); | |||
| xpathEngine = XPathFactory.newInstance().newXPath(); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| CommonLogger.alarm.error(e.getMessage()); | |||
| throw new PlatformException(PlatformError.RUNTIME_ERROR, e.getMessage()); | |||
| } | |||
| // parse request into multiple BroadcastSMS pass then to Dispatcher | |||
| Document xmlDoc; | |||
| try | |||
| { | |||
| InputStream inb; | |||
| String contentType = request.getContentType(); | |||
| if (contentType != null && contentType.split("/")[1].equalsIgnoreCase("zip")) | |||
| { | |||
| ZipInputStream zip = new ZipInputStream(request.getInputStream()); | |||
| zip.getNextEntry(); | |||
| inb = zip; | |||
| } | |||
| else | |||
| { | |||
| inb = request.getInputStream(); | |||
| } | |||
| int contentLength = request.getContentLength(); | |||
| CommonLogger.activity.info("Receiving " + contentLength + " bytes of data"); | |||
| byte[] content = new byte[contentLength]; | |||
| int offset = 0; | |||
| int length = contentLength; | |||
| int read; | |||
| while (length > 0) | |||
| { | |||
| read = inb.read(content, offset, length); | |||
| if (read < 0) break; | |||
| offset += read; | |||
| length -= read; | |||
| } | |||
| CommonLogger.activity.info("Received: " + new String(content, "UTF-8")); | |||
| xmlDoc = builder.parse(new ByteArrayInputStream(content), "UTF-8"); | |||
| } | |||
| catch (SAXException e) | |||
| { | |||
| throw new BroadcastException(BroadcastError.BAD_REQUEST_DATA, e.getMessage()); | |||
| } | |||
| catch (IOException e) | |||
| { | |||
| throw new BroadcastException(BroadcastError.READ_REQUEST_ERROR, "Problem in reading request"); | |||
| } | |||
| //String xpath = "//" + broadcastName; // get all first level elements | |||
| NodeList broadcastNodes = null; | |||
| broadcastNodes = xmlDoc.getElementsByTagName(broadcastType); | |||
| if (broadcastNodes.getLength() == 0) | |||
| { | |||
| throw new BroadcastException(BroadcastError.BAD_REQUEST_DATA, "No <" + broadcastType + "> tag"); | |||
| } | |||
| if (notInService) return; | |||
| // Only one broadcast node is processed. The rest are silently ignored. | |||
| broadcastNode = (Element)broadcastNodes.item(0); | |||
| if (!broadcastType.equals(broadcastNode.getNodeName())) | |||
| { | |||
| throw new BroadcastMsgException("Broadcast node name does not match " + broadcastType); | |||
| } | |||
| // broadcast_id | |||
| String broadcastId = broadcastNode.getAttribute("broadcast_id"); | |||
| if (broadcastId.length() == 0) | |||
| { | |||
| throw new BroadcastMsgException("Missing broadcast_id"); | |||
| } | |||
| setBroadcastId(broadcastId); | |||
| // expireTime | |||
| long now = System.currentTimeMillis(); | |||
| try | |||
| { | |||
| long expireTime = getLongValue("expire_time", broadcastNode); | |||
| // defaults to 20 minutes | |||
| myLogger.debug("expire_time decoded to be " + expireTime); | |||
| if (expireTime == 0) expireTime = now + 20 * 60 * 1000; | |||
| myLogger.debug("expire time adjusted to be " + expireTime); | |||
| setExpireTime(expireTime); | |||
| setLaunchRecordId(broadcastNode.getAttribute("launch_record_id")); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| throw new BroadcastMsgException("Missing or bad expire_time"); | |||
| } | |||
| // Postback | |||
| postBackURL = getStringValue("async_status_post_back", broadcastNode); | |||
| if (postBackURL != null && (postBackURL=postBackURL.trim()).length() == 0) postBackURL = null; | |||
| if (postBackURL == null) | |||
| { | |||
| CommonLogger.alarm.warn("Missing asyn_status_post_back in POST data"); | |||
| } | |||
| NodeList recipientNodes = null; | |||
| String xpath = "recipient_list/recipient"; | |||
| try | |||
| { | |||
| recipientNodes = (NodeList) xpathEngine.evaluate(xpath, | |||
| broadcastNode, XPathConstants.NODESET); | |||
| } | |||
| catch (XPathExpressionException e) | |||
| { | |||
| throw new InternalErrorException("Bad xpath 'recipient_list/recipient", e); | |||
| } | |||
| Element recipientNode; | |||
| String contact_id; | |||
| String activity_record_id; | |||
| xpath = "email"; | |||
| for (int i = 0; i < recipientNodes.getLength(); i++) | |||
| { | |||
| recipientNode = (Element)recipientNodes.item(i); | |||
| contact_id = recipientNode.getAttribute("contact_id"); | |||
| if (contact_id == null || (contact_id=contact_id.trim()).length() == 0) | |||
| { | |||
| myLogger.warn("Missing or empty contact_id for a recipient in broadcast " + getBroadcastId()); | |||
| continue; | |||
| } | |||
| activity_record_id = recipientNode.getAttribute(getActivityRecordIdName()); | |||
| if (activity_record_id == null || (activity_record_id = activity_record_id.trim()).length() == 0) | |||
| { | |||
| throw new BroadcastMsgException("Missing or empty " + getActivityRecordIdName() + " attribute for a recipient in broadcast " + getBroadcastId()); | |||
| } | |||
| Map<String, String> attributes = new HashMap<String, String>(); | |||
| NodeList children = recipientNode.getChildNodes(); | |||
| for (int j = 0; j < children.getLength(); j++) | |||
| { | |||
| Node child = children.item(j); | |||
| if (child.getNodeType() != Node.ELEMENT_NODE) continue; | |||
| attributes.put(child.getNodeName(), child.getTextContent()); | |||
| } | |||
| recipientList.add(new Recipient(contact_id, activity_record_id, attributes)); | |||
| } | |||
| } | |||
| /** | |||
| * Gets String value of child with given name. | |||
| * @param childNodeName | |||
| * @param element | |||
| * @return null if no such child exists. | |||
| * @throws IllegalArgumentException when element is null | |||
| */ | |||
| protected String getStringValue(String childNodeName, Element element) | |||
| throws IllegalArgumentException | |||
| { | |||
| if (element == null) | |||
| { | |||
| throw new IllegalArgumentException("Element cannot be null"); | |||
| } | |||
| NodeList children = element.getChildNodes(); | |||
| for (int i = 0; i < children.getLength(); i++) | |||
| { | |||
| Node child = children.item(i); | |||
| if (child.getNodeType() == Node.ELEMENT_NODE && | |||
| child.getNodeName().equals(childNodeName)) | |||
| { | |||
| return child.getTextContent(); | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * | |||
| * @param childNodeName | |||
| * @param element | |||
| * @return | |||
| * @throws IllegalArgumentException when element is null | |||
| */ | |||
| protected String getNonNullStringValue(String childNodeName, Element element) | |||
| throws IllegalArgumentException | |||
| { | |||
| String value = getStringValue(childNodeName, element); | |||
| return value==null?"":value; | |||
| } | |||
| /** | |||
| * | |||
| * @param childNodeName | |||
| * @param element | |||
| * @return | |||
| * @throws IllegalArgumentException when element is null | |||
| */ | |||
| public boolean getBooleanValue(String childNodeName, Element element) | |||
| throws IllegalArgumentException | |||
| { | |||
| String str = getStringValue(childNodeName, element); | |||
| if (str == null) return false; | |||
| return Boolean.parseBoolean(str.trim()); | |||
| } | |||
| /** | |||
| * | |||
| * @param childNodeName | |||
| * @param element | |||
| * @return | |||
| * @throws NonExistentException | |||
| * @throws NumberFormatException | |||
| * @throws IllegalArgumentException when element is null | |||
| */ | |||
| protected int getIntValue(String childNodeName, Element element) | |||
| throws NonExistentException, NumberFormatException, IllegalArgumentException | |||
| { | |||
| if (element == null) | |||
| { | |||
| throw new IllegalArgumentException("Element cannot be null, when invoking getIntValue method"); | |||
| } | |||
| try | |||
| { | |||
| String str = getStringValue(childNodeName, element); | |||
| if (str == null) | |||
| { | |||
| throw new NonExistentException("No child \"" + childNodeName + "\" exists for element \"" + xml2Str(element) + "\""); | |||
| } | |||
| return Integer.parseInt(str); | |||
| } | |||
| catch (NumberFormatException e) | |||
| { | |||
| throw new NumberFormatException("Value of child \"" + childNodeName + "\" not integer for element \"" + xml2Str(element) + "\""); | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @param childNodeName | |||
| * @param element | |||
| * @return | |||
| * @throws NonExistentException | |||
| * @throws NumberFormatException | |||
| * @throws IllegalArgumentException when element is null | |||
| */ | |||
| protected Long getLongValue(String childNodeName, Element element) | |||
| throws NonExistentException, NumberFormatException, IllegalArgumentException | |||
| { | |||
| if (element == null) | |||
| { | |||
| throw new IllegalArgumentException("Element cannot be null, when invoking getIntValue method"); | |||
| } | |||
| try | |||
| { | |||
| String str = getStringValue(childNodeName, element); | |||
| if (str == null) | |||
| { | |||
| throw new NonExistentException("No child \"" + childNodeName + "\" exists for element \"" + xml2Str(element) + "\""); | |||
| } | |||
| return Long.parseLong(str); | |||
| } | |||
| catch (NumberFormatException e) | |||
| { | |||
| throw new NumberFormatException("Value of child \"" + childNodeName + "\" not integer for element \"" + xml2Str(element) + "\""); | |||
| } | |||
| } | |||
| protected NodeList getNodeList(String xpath, Element element) | |||
| { | |||
| try | |||
| { | |||
| NodeList nodes = (NodeList) xpathEngine.evaluate(xpath, element, XPathConstants.NODESET); | |||
| return nodes; | |||
| } | |||
| catch (XPathExpressionException e) | |||
| { | |||
| throw new InternalErrorException("Improper xpath \"" + xpath + "\"", e); | |||
| } | |||
| } | |||
| protected Node getNode(String xpath, Element element) | |||
| { | |||
| try | |||
| { | |||
| Node node = (Node) xpathEngine.evaluate(xpath, element, XPathConstants.NODE); | |||
| return node; | |||
| } | |||
| catch (XPathExpressionException e) | |||
| { | |||
| throw new InternalErrorException("Improper xpath \"" + xpath + "\"", e); | |||
| } | |||
| } | |||
| static public void appendXML(Element topElement, StringBuffer buf) | |||
| { | |||
| // starting tag | |||
| buf.append('<'); | |||
| String elementName = topElement.getNodeName(); | |||
| buf.append(elementName); | |||
| // handle attributes first | |||
| NamedNodeMap attributes = topElement.getAttributes(); | |||
| for (int i = 0; i < attributes.getLength(); i++) | |||
| { | |||
| Node attr = attributes.item(i); | |||
| buf.append(' '); | |||
| buf.append(attr.getNodeName()); | |||
| buf.append("=\""); | |||
| buf.append(attr.getNodeValue()); | |||
| buf.append("\""); | |||
| } | |||
| buf.append('>'); | |||
| // handling text | |||
| Vector<Node> childElements = new Vector<Node>(); | |||
| NodeList children = topElement.getChildNodes(); | |||
| for (int i = 0; i < children.getLength(); i++) | |||
| { | |||
| Node node = children.item(i); | |||
| switch (node.getNodeType()) | |||
| { | |||
| case Node.ELEMENT_NODE: | |||
| // defer handling | |||
| childElements.add(node); | |||
| break; | |||
| case Node.TEXT_NODE: | |||
| buf.append(node.getNodeValue()); | |||
| break; | |||
| default: | |||
| } | |||
| } | |||
| // handling children elements | |||
| for (Node child : childElements) | |||
| { | |||
| appendXML((Element)child, buf); | |||
| } | |||
| // ending tag | |||
| buf.append("</"); buf.append(elementName); buf.append('>'); | |||
| } | |||
| public String xml2Str(Element element) | |||
| { | |||
| StringBuffer buf = new StringBuffer(); | |||
| appendXML(element, buf); | |||
| return buf.toString(); | |||
| } | |||
| /** | |||
| * Derived class may override this method to redefine the name of activity_record_id, | |||
| * e.g. email_record_id, sms_record_id, etc. | |||
| * @return name of activity_record_9d. | |||
| */ | |||
| protected String getActivityRecordIdName() | |||
| { | |||
| return "activity_record_id"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| package altk.comm.engine.exception; | |||
| public enum BroadcastError | |||
| { | |||
| EXPIRED, | |||
| READ_REQUEST_ERROR, | |||
| BAD_REQUEST_DATA, | |||
| INTERNAL_ERROR, | |||
| CONFIGURAION_ERROR, | |||
| PLATFORM_ERROR | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class BroadcastException extends EngineException | |||
| { | |||
| public final BroadcastError errorCode; | |||
| public BroadcastException(BroadcastError errorCode, String errorText) | |||
| { | |||
| this(errorCode, errorText, null); | |||
| } | |||
| public BroadcastException(BroadcastError errorCode, String errorText, Throwable t) | |||
| { | |||
| super(errorCode.toString(), errorText, t); | |||
| this.errorCode = errorCode; | |||
| } | |||
| /* | |||
| public BroadcastException(Broadcast broadcast, BroadcastError errorCode, String errorText) | |||
| { | |||
| this(broadcast.broadcastName, broadcast.broadcastId, errorCode, errorText); | |||
| } | |||
| public BroadcastException(String broadcastName, String broadcastId, | |||
| BroadcastError errorCode, String errorText) | |||
| { | |||
| this(broadcastName, broadcastId, errorCode, errorText, null); | |||
| } | |||
| public BroadcastException(Broadcast broadcast, BroadcastError errorCode, String errorText, Throwable t) | |||
| { | |||
| this(broadcast.broadcastName, broadcast.broadcastId, errorCode, errorText, t); | |||
| } | |||
| public BroadcastException(String broadcastName, String broadcastId, | |||
| BroadcastError errorCode, String errorText, Throwable t) | |||
| { | |||
| super(errorCode.toString() + ": " + errorText, t); | |||
| this.broadcastName = broadcastName; | |||
| this.broadcastId = broadcastId; | |||
| this.errorCode = errorCode; | |||
| this.errorText = errorText; | |||
| } | |||
| public String mkResponseXML() | |||
| { | |||
| StringBuffer responseXML = new StringBuffer("<" + broadcastName + "_response"); | |||
| if (broadcastId != null && broadcastId.length() > 0) | |||
| { | |||
| responseXML.append(" broadcast_id=\""); | |||
| responseXML.append(broadcastId); | |||
| responseXML.append("\""); | |||
| } | |||
| if (errorCode != null) | |||
| { | |||
| responseXML.append(" error=\""); | |||
| responseXML.append(errorCode.toString()); | |||
| responseXML.append("\""); | |||
| if (errorText != null) | |||
| { | |||
| responseXML.append("><error_text>"); | |||
| responseXML.append(errorText); | |||
| responseXML.append("</error_text></" + broadcastName + "_response>"); | |||
| } | |||
| else | |||
| { | |||
| responseXML.append("/>"); | |||
| } | |||
| } | |||
| return responseXML.toString(); | |||
| } | |||
| */ | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class BroadcastExpiredException extends BroadcastException | |||
| { | |||
| public BroadcastExpiredException(String errorText) | |||
| { | |||
| super(BroadcastError.EXPIRED, errorText); | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| package altk.comm.engine.exception; | |||
| import altk.comm.engine.Broadcast; | |||
| @SuppressWarnings("serial") | |||
| public class BroadcastInternalErrorException extends BroadcastException | |||
| { | |||
| public BroadcastInternalErrorException(Broadcast broadcast, String errorText) | |||
| { | |||
| super(BroadcastError.INTERNAL_ERROR, errorText); | |||
| } | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class BroadcastMsgException extends BroadcastException | |||
| { | |||
| public BroadcastMsgException(String errorText) | |||
| { | |||
| this(errorText, null); | |||
| } | |||
| public BroadcastMsgException(String errorText, Throwable t) | |||
| { | |||
| super(BroadcastError.BAD_REQUEST_DATA, errorText, t); | |||
| } | |||
| /* | |||
| public BroadcastMsgException(Broadcast broadcast, String errorText) | |||
| { | |||
| super(broadcast, BroadcastError.BAD_REQUEST_DATA, errorText); | |||
| } | |||
| public BroadcastMsgException(Broadcast broadcast, String errorText, Throwable t) | |||
| { | |||
| super(broadcast, BroadcastError.BAD_REQUEST_DATA, errorText, t); | |||
| } | |||
| */ | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| package altk.comm.engine.exception; | |||
| /** | |||
| * Base class of Exceptions used in this platform. Derived classes use | |||
| * their own enum for errorCode. | |||
| * | |||
| * @author Yuk-Ming | |||
| * | |||
| */ | |||
| @SuppressWarnings("serial") | |||
| public abstract class EngineException extends Exception | |||
| { | |||
| public final String errorCodeText; | |||
| public final String errorText; | |||
| public EngineException(String errorCodeText, String errorText) | |||
| { | |||
| this(errorCodeText, errorText, null); | |||
| } | |||
| public EngineException(String errorCodeText, String errorText, Throwable cause) | |||
| { | |||
| super(errorCodeText + ": " + errorText, cause); | |||
| this.errorCodeText = errorCodeText; | |||
| this.errorText = errorText; | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class InternalErrorException extends RuntimeException | |||
| { | |||
| public InternalErrorException(Throwable t) | |||
| { | |||
| super(t); | |||
| } | |||
| public InternalErrorException(String message) | |||
| { | |||
| super(message); | |||
| } | |||
| public InternalErrorException(String message, Exception e) | |||
| { | |||
| super(message, e); | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class NonExistentException extends Exception | |||
| { | |||
| public NonExistentException(String msg) | |||
| { | |||
| super(msg); | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| package altk.comm.engine.exception; | |||
| public enum PlatformError | |||
| { | |||
| INTERNAL_ERROR, | |||
| CONFIGURATION_ERROR, | |||
| RUNTIME_ERROR | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| package altk.comm.engine.exception; | |||
| @SuppressWarnings("serial") | |||
| public class PlatformException extends EngineException | |||
| { | |||
| public final PlatformError errorCode; | |||
| public PlatformException(PlatformError errorCode, String errorText) | |||
| { | |||
| this(errorCode, errorText, null); | |||
| } | |||
| public PlatformException(PlatformError errorCode, String errorText, Throwable t) | |||
| { | |||
| super(errorCode.toString(), errorText, t); | |||
| this.errorCode = errorCode; | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| package altk.comm.engine.exception; | |||
| import altk.comm.engine.Job.JobStatus; | |||
| @SuppressWarnings("serial") | |||
| public class ServiceException extends EngineException | |||
| { | |||
| public final JobStatus status; | |||
| public ServiceException(JobStatus status, String errorText) | |||
| { | |||
| this(status, errorText, null); | |||
| } | |||
| public ServiceException(JobStatus status, String errorText, Throwable t) | |||
| { | |||
| super(status.toString(), errorText, t); | |||
| this.status = status; | |||
| } | |||
| } | |||
| @@ -0,0 +1,416 @@ | |||
| package altk.comm.engine.postback; | |||
| import java.io.ByteArrayInputStream; | |||
| import java.io.IOException; | |||
| import java.io.UnsupportedEncodingException; | |||
| import java.util.ArrayList; | |||
| import java.util.Iterator; | |||
| import java.util.LinkedList; | |||
| import java.util.List; | |||
| import java.util.Queue; | |||
| import javax.xml.parsers.DocumentBuilder; | |||
| import javax.xml.parsers.DocumentBuilderFactory; | |||
| import javax.xml.xpath.XPath; | |||
| import javax.xml.xpath.XPathConstants; | |||
| import javax.xml.xpath.XPathExpressionException; | |||
| import javax.xml.xpath.XPathFactory; | |||
| import org.apache.commons.httpclient.ConnectTimeoutException; | |||
| import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; | |||
| import org.apache.commons.httpclient.HttpClient; | |||
| import org.apache.commons.httpclient.methods.PostMethod; | |||
| import org.apache.commons.httpclient.methods.StringRequestEntity; | |||
| import org.apache.commons.httpclient.params.HttpMethodParams; | |||
| import org.apache.log4j.Logger; | |||
| import org.w3c.dom.Document; | |||
| import org.w3c.dom.Node; | |||
| import altk.comm.engine.CommonLogger; | |||
| /** | |||
| * Queues JobReports to be posted back to attribute postBackURL. | |||
| * Multiple internal class Sender members consume this postQueue, sending items | |||
| * in postQueue to postBackURL. | |||
| * | |||
| * In the future, if postBackURL has problem, or if | |||
| * length of postQueue is more than a MAX_QUEUE_LENGTH, then it starts writing | |||
| * everything to backingFile. | |||
| * | |||
| * @author Kwong | |||
| * | |||
| */ | |||
| public class PostBack | |||
| { | |||
| private static final String XML_VERSION_1_0_ENCODING_UTF_8 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; | |||
| private static final int QUEUE_WAIT = 300; // seconds | |||
| private static final int POSTBACK_SERVER_WAIT_TIME = 10; // seconds | |||
| private static final int THREADPOOL_SIZE_DEFAULT = 2; | |||
| private static final int MAX_QUEUE_SIZE_DEFAULT = 10000; | |||
| private static final int MAX_BATCH_SIZE_DEFAULT = 100; | |||
| private final String postBackURL; | |||
| private final String xmlTopElement; | |||
| private Queue<String> postQueue; | |||
| private final int maxQueueSize; | |||
| private final int senderPoolSize; | |||
| private List<Sender> senderPool; | |||
| private final String myName; | |||
| private int maxBatchSize; | |||
| private static Logger myLogger = Logger.getLogger(PostBack.class); | |||
| public enum PostBackStatus | |||
| { | |||
| SUCCESS, | |||
| SERVER_IO_ERROR, | |||
| IRRECOVERABLE_ERROR, | |||
| HTTP_STATUS_ERROR | |||
| } | |||
| class Sender extends Thread | |||
| { | |||
| private boolean threadShouldStop; | |||
| private Sender(String name) | |||
| { | |||
| setName(name); | |||
| start(); | |||
| } | |||
| public void run() | |||
| { | |||
| threadShouldStop = false; | |||
| myLogger.info(getName() + " started"); | |||
| String report; | |||
| for (;;) // Each iteration sends a batch | |||
| { | |||
| if (threadShouldStop) | |||
| { | |||
| myLogger.info(getName() + " terminating"); | |||
| System.out.println(getName() + " terminating"); | |||
| return; | |||
| } | |||
| List<String> reportList = null; | |||
| synchronized(postQueue) | |||
| { | |||
| // Each iteration examines the queue for a batch to send | |||
| for (;;) | |||
| { | |||
| reportList = new ArrayList<String>(); | |||
| for (int i = 0; i < maxBatchSize ; i++) | |||
| { | |||
| report = postQueue.poll(); | |||
| if (report == null) break; | |||
| reportList.add(report); | |||
| } | |||
| if (reportList.size() > 0) break; // break out to do the work. | |||
| // Nothing to do, so wait a while, and look at the | |||
| // queue again. | |||
| try | |||
| { | |||
| postQueue.wait(QUEUE_WAIT * 1000); | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| CommonLogger.alarm.info(getName() + ": Postback queue interrupted while waiting: " + e); | |||
| break; | |||
| } | |||
| CommonLogger.health.info(getName() + " surfacing from wait"); | |||
| System.out.println(getName() + " surfacing from wait"); | |||
| continue; | |||
| } | |||
| } // synchronized() | |||
| if (reportList != null && reportList.size() > 0) | |||
| { | |||
| switch (post(reportList)) | |||
| { | |||
| case IRRECOVERABLE_ERROR: | |||
| case SUCCESS: | |||
| break; | |||
| case SERVER_IO_ERROR: | |||
| // TODO: Limit retries, using rate limiting. Posting can be recovered using the activity log. | |||
| // Re-queue this job | |||
| queueReports(reportList); | |||
| // Sleep for a while before retrying this PostBack server. | |||
| CommonLogger.alarm.warn(getName() + ": Caught server IO error. sleep for " + POSTBACK_SERVER_WAIT_TIME + " seconds"); | |||
| try | |||
| { | |||
| Thread.sleep(POSTBACK_SERVER_WAIT_TIME * 1000); | |||
| } | |||
| catch (InterruptedException e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": Caught while PostBack thread sleeps: " + e); | |||
| } | |||
| default: | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @param reportList | |||
| * @return SUCCESS, | |||
| * SERVER_IO_ERROR, when postback receiver has problem | |||
| * IRRECOVERABLE_ERROR | |||
| */ | |||
| private PostBackStatus post(List<String> reportList) | |||
| { | |||
| StringBuffer xml = new StringBuffer(XML_VERSION_1_0_ENCODING_UTF_8); | |||
| xml.append("<"); xml.append(xmlTopElement); xml.append(">"); | |||
| for (String report : reportList) | |||
| { | |||
| xml.append(report); | |||
| } | |||
| xml.append("</"); xml.append(xmlTopElement); xml.append(">"); | |||
| CommonLogger.activity.info(getName() + ": posting " + xml.toString()); | |||
| PostMethod post = new PostMethod(postBackURL); | |||
| String responseBody = null; | |||
| try | |||
| { | |||
| post.setRequestEntity(new StringRequestEntity(xml.toString(), | |||
| "application/xml", "utf-8")); | |||
| } | |||
| catch (UnsupportedEncodingException e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": While adding this application/xml content to PostBack: " + xml + " -- " + e); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| post.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, | |||
| new DefaultHttpMethodRetryHandler(3, false)); | |||
| HttpClient client = new HttpClient(); | |||
| try | |||
| { | |||
| client.getHttpConnectionManager().getParams().setConnectionTimeout(5 * 1000); | |||
| int statusCode = client.executeMethod(post); | |||
| if (statusCode != 200) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": Received problem status code " + statusCode + " from posting to \"" + postBackURL + "\": " + xml); | |||
| return PostBackStatus.HTTP_STATUS_ERROR; | |||
| } | |||
| responseBody = post.getResponseBodyAsString().trim(); | |||
| CommonLogger.activity.info(getName() + ": Received response: " + (responseBody.length() == 0? "[empty]" : responseBody)); | |||
| if (responseBody.trim().length() == 0) return PostBackStatus.SUCCESS; | |||
| } | |||
| catch (ConnectTimeoutException e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": IO problem while posting to \"" + postBackURL + "\": " + xml + " -- " + e.getMessage()); | |||
| return PostBackStatus.SERVER_IO_ERROR; | |||
| } | |||
| catch (IOException e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": IO problem while posting to \"" + postBackURL + "\": " + xml + " -- " + e.getMessage()); | |||
| return PostBackStatus.SERVER_IO_ERROR; | |||
| } | |||
| catch (IllegalArgumentException e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": When posting to \"" + postBackURL + "\": " + e.getMessage()); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| // parse into xml doc | |||
| Document xmlDoc = null; | |||
| try | |||
| { | |||
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |||
| DocumentBuilder builder = factory.newDocumentBuilder(); | |||
| xmlDoc = builder.parse(new ByteArrayInputStream(responseBody.getBytes())); | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": xml parse problem on received response from " + postBackURL + ": " + responseBody); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| if (!xmlDoc.getDocumentElement().getNodeName().startsWith(xmlTopElement)) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": xml response from " + postBackURL + " not a <" + xmlTopElement + "> response: " + responseBody); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| XPath xpathEngine = XPathFactory.newInstance().newXPath(); | |||
| String xpath = null; | |||
| try | |||
| { | |||
| xpath = "@error"; | |||
| Node errorNode = (Node)xpathEngine.evaluate(xpath, xmlDoc.getDocumentElement(), XPathConstants.NODE); | |||
| if (errorNode != null) | |||
| { | |||
| String errorCode = errorNode.getNodeValue(); | |||
| xpath = "error_text"; | |||
| String errorText = (String)xpathEngine.evaluate(xpath, | |||
| xmlDoc.getDocumentElement(), XPathConstants.STRING); | |||
| CommonLogger.alarm.warn(getName() + ": Error response to <" + xmlTopElement + "> post back to " | |||
| + postBackURL + " -- error code=\"" + errorCode + "\", error text = \"" | |||
| + errorText + "\""); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| } | |||
| catch (XPathExpressionException e) | |||
| { | |||
| CommonLogger.alarm.warn("Bad xpath: " + xpath); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| CommonLogger.alarm.warn(getName() + ": While decoding post back response from server: " + e); | |||
| return PostBackStatus.IRRECOVERABLE_ERROR; | |||
| } | |||
| return PostBackStatus.SUCCESS; | |||
| } | |||
| public void terminate() | |||
| { | |||
| if (threadShouldStop) return; | |||
| threadShouldStop = true; | |||
| //Wait for at most 100 ms for thread to stop | |||
| interrupt(); | |||
| } | |||
| } | |||
| /** | |||
| * Constructs a pool of threads doing posting from a common job queue, | |||
| * to the supplied postBackURL. The top element of the XML that gets | |||
| * posted back has the give name. | |||
| * | |||
| * Requires these System properties: | |||
| * postback_max_queue_size | |||
| * postback_threadpool_size | |||
| * | |||
| * @param postBackURL | |||
| * @param xmlTopElementName | |||
| * @throws IllegalArgumentException if either postBackURL or xmlTopElementName is | |||
| * not supplied nor valid. | |||
| */ | |||
| public PostBack(String postBackURL, String xmlTopElementName) throws IllegalArgumentException | |||
| { | |||
| if (postBackURL == null || postBackURL.length() == 0) | |||
| { | |||
| throw new IllegalArgumentException("PostBack class given null postBackURL"); | |||
| } | |||
| myName = "Postback-" + postBackURL; | |||
| if (xmlTopElementName == null || xmlTopElementName.length() == 0) | |||
| { | |||
| throw new IllegalArgumentException(myName + ": PostBack class given null xmlTopElement"); | |||
| } | |||
| this.postBackURL = postBackURL; | |||
| this.xmlTopElement = xmlTopElementName; | |||
| postQueue = new LinkedList<String>(); | |||
| int max_queue_size = 0; | |||
| String maxQueueSizeStr = System.getProperty("postback_max_queue_size"); | |||
| if (maxQueueSizeStr != null && (maxQueueSizeStr=maxQueueSizeStr.trim()).length() > 0) | |||
| { | |||
| max_queue_size = Integer.parseInt(maxQueueSizeStr); | |||
| } | |||
| maxQueueSize = max_queue_size > 0? max_queue_size : MAX_QUEUE_SIZE_DEFAULT; | |||
| CommonLogger.activity.info("Postback max queue size = " + maxQueueSize); | |||
| int poolSize = 0; | |||
| String senderSizeStr = System.getProperty("postback_threadpool_size"); | |||
| if (senderSizeStr != null && (senderSizeStr=senderSizeStr.trim()).length() > 0) | |||
| { | |||
| poolSize = Integer.parseInt(senderSizeStr); | |||
| } | |||
| senderPoolSize = poolSize > 0? poolSize : THREADPOOL_SIZE_DEFAULT; | |||
| CommonLogger.activity.info("Postback threadpool size = " + senderPoolSize); | |||
| int configuredMax = 0; | |||
| String maxBatchSizeStr = System.getProperty("postback_max_batch_size"); | |||
| if (maxBatchSizeStr != null && (maxBatchSizeStr=maxBatchSizeStr.trim()).length() > 0) | |||
| { | |||
| configuredMax = Integer.parseInt(maxBatchSizeStr); | |||
| } | |||
| maxBatchSize = configuredMax > 0? configuredMax: MAX_BATCH_SIZE_DEFAULT; | |||
| CommonLogger.activity.info("Postback max batch size = " + maxBatchSize); | |||
| senderPool = new ArrayList<Sender>(); | |||
| for (int i = 0; i < senderPoolSize; i++) | |||
| { | |||
| Sender sender = new Sender(myName + '-' + i); | |||
| senderPool.add(sender); | |||
| } | |||
| } | |||
| /** | |||
| * Queues report to postQueue only if the queue size has not reached the | |||
| * maxQueueSize. | |||
| * @param report | |||
| * @return true if report is added to queue, false otherwise (queue full) | |||
| */ | |||
| public boolean queueReport(String report) | |||
| { | |||
| // Log for recovery in case of problem in posting report. | |||
| CommonLogger.activity.info(myName + " queing report: " + report); | |||
| myLogger.debug(myName + ": postQueue size: " + postQueue.size()); | |||
| synchronized(postQueue) | |||
| { | |||
| if (postQueue.size() < maxQueueSize) | |||
| { | |||
| postQueue.add(report); | |||
| postQueue.notify(); | |||
| return true; | |||
| } | |||
| } | |||
| CommonLogger.alarm.warn(myName + ".queueReport method returning false"); | |||
| return false; | |||
| } | |||
| /** | |||
| * Queues reports to postQueue only if the queue size has not reached the | |||
| * maxQueueSize. | |||
| * @param reports to be added back to postQueue | |||
| * @return true if all jobs have been added to queue, false otherwise (queue full) | |||
| */ | |||
| public boolean queueReports(List<String> reports) | |||
| { | |||
| myLogger.debug(myName + ": postQueue size: " + postQueue.size()); | |||
| synchronized(postQueue) | |||
| { | |||
| Iterator<String> iter = reports.iterator(); | |||
| int count = 0; // Number of reports added back to postQueue | |||
| while (iter.hasNext()) | |||
| { | |||
| String report = iter.next(); | |||
| if (postQueue.size() < maxQueueSize) | |||
| { | |||
| postQueue.add(report); | |||
| count++; | |||
| } | |||
| } | |||
| if (count > 0) postQueue.notify(); | |||
| boolean returnValue = (count == reports.size()); | |||
| if (!returnValue) | |||
| { | |||
| CommonLogger.alarm.warn(myName | |||
| + ".queueReport method returning false, having queued " | |||
| + count + " out of " + reports.size()); | |||
| } | |||
| return returnValue; | |||
| } | |||
| } | |||
| public void terminate() | |||
| { | |||
| for (Sender sender : senderPool) | |||
| { | |||
| sender.terminate(); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,145 @@ | |||
| package altk.common.engine.util; | |||
| /** | |||
| * A Base64 Encoder/Decoder. | |||
| * | |||
| * <p> | |||
| * This class is used to encode and decode data in Base64 format as described in RFC 1521. | |||
| * | |||
| * <p> | |||
| * This is "Open Source" software and released under the <a href="http://www.gnu.org/licenses/lgpl.html">GNU/LGPL</a> license.<br> | |||
| * It is provided "as is" without warranty of any kind.<br> | |||
| * Copyright 2003: Christian d'Heureuse, Inventec Informatik AG, Switzerland.<br> | |||
| * Home page: <a href="http://www.source-code.biz">www.source-code.biz</a><br> | |||
| * | |||
| * <p> | |||
| * Version history:<br> | |||
| * 2003-07-22 Christian d'Heureuse (chdh): Module created.<br> | |||
| * 2005-08-11 chdh: Lincense changed from GPL to LGPL.<br> | |||
| * 2006-11-21 chdh:<br> | |||
| * Method encode(String) renamed to encodeString(String).<br> | |||
| * Method decode(String) renamed to decodeString(String).<br> | |||
| * New method encode(byte[],int) added.<br> | |||
| * New method decode(String) added.<br> | |||
| */ | |||
| public class Base64Coder { | |||
| // Mapping table from 6-bit nibbles to Base64 characters. | |||
| private static char[] map1 = new char[64]; | |||
| static { | |||
| int i=0; | |||
| for (char c='A'; c<='Z'; c++) map1[i++] = c; | |||
| for (char c='a'; c<='z'; c++) map1[i++] = c; | |||
| for (char c='0'; c<='9'; c++) map1[i++] = c; | |||
| map1[i++] = '+'; map1[i++] = '/'; } | |||
| // Mapping table from Base64 characters to 6-bit nibbles. | |||
| private static byte[] map2 = new byte[128]; | |||
| static { | |||
| for (int i=0; i<map2.length; i++) map2[i] = -1; | |||
| for (int i=0; i<64; i++) map2[map1[i]] = (byte)i; } | |||
| /** | |||
| * Encodes a string into Base64 format. | |||
| * No blanks or line breaks are inserted. | |||
| * @param s a String to be encoded. | |||
| * @return A String with the Base64 encoded data. | |||
| */ | |||
| public static String encodeString (String s) { | |||
| return new String(encode(s.getBytes())); } | |||
| /** | |||
| * Encodes a byte array into Base64 format. | |||
| * No blanks or line breaks are inserted. | |||
| * @param in an array containing the data bytes to be encoded. | |||
| * @return A character array with the Base64 encoded data. | |||
| */ | |||
| public static char[] encode (byte[] in) { | |||
| return encode(in,in.length); } | |||
| /** | |||
| * Encodes a byte array into Base64 format. | |||
| * No blanks or line breaks are inserted. | |||
| * @param in an array containing the data bytes to be encoded. | |||
| * @param iLen number of bytes to process in <code>in</code>. | |||
| * @return A character array with the Base64 encoded data. | |||
| */ | |||
| public static char[] encode (byte[] in, int iLen) { | |||
| int oDataLen = (iLen*4+2)/3; // output length without padding | |||
| int oLen = ((iLen+2)/3)*4; // output length including padding | |||
| char[] out = new char[oLen]; | |||
| int ip = 0; | |||
| int op = 0; | |||
| while (ip < iLen) { | |||
| int i0 = in[ip++] & 0xff; | |||
| int i1 = ip < iLen ? in[ip++] & 0xff : 0; | |||
| int i2 = ip < iLen ? in[ip++] & 0xff : 0; | |||
| int o0 = i0 >>> 2; | |||
| int o1 = ((i0 & 3) << 4) | (i1 >>> 4); | |||
| int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); | |||
| int o3 = i2 & 0x3F; | |||
| out[op++] = map1[o0]; | |||
| out[op++] = map1[o1]; | |||
| out[op] = op < oDataLen ? map1[o2] : '='; op++; | |||
| out[op] = op < oDataLen ? map1[o3] : '='; op++; } | |||
| return out; } | |||
| /** | |||
| * Decodes a string from Base64 format. | |||
| * @param s a Base64 String to be decoded. | |||
| * @return A String containing the decoded data. | |||
| * @throws IllegalArgumentException if the input is not valid Base64 encoded data. | |||
| */ | |||
| public static String decodeString (String s) { | |||
| return new String(decode(s)); } | |||
| /** | |||
| * Decodes a byte array from Base64 format. | |||
| * @param s a Base64 String to be decoded. | |||
| * @return An array containing the decoded data bytes. | |||
| * @throws IllegalArgumentException if the input is not valid Base64 encoded data. | |||
| */ | |||
| public static byte[] decode (String s) { | |||
| return decode(s.toCharArray()); } | |||
| /** | |||
| * Decodes a byte array from Base64 format. | |||
| * No blanks or line breaks are allowed within the Base64 encoded data. | |||
| * @param in a character array containing the Base64 encoded data. | |||
| * @return An array containing the decoded data bytes. | |||
| * @throws IllegalArgumentException if the input is not valid Base64 encoded data. | |||
| */ | |||
| public static byte[] decode (char[] in) { | |||
| int iLen = in.length; | |||
| if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 encoded input string is not a multiple of 4."); | |||
| while (iLen > 0 && in[iLen-1] == '=') iLen--; | |||
| int oLen = (iLen*3) / 4; | |||
| byte[] out = new byte[oLen]; | |||
| int ip = 0; | |||
| int op = 0; | |||
| while (ip < iLen) { | |||
| int i0 = in[ip++]; | |||
| int i1 = in[ip++]; | |||
| int i2 = ip < iLen ? in[ip++] : 'A'; | |||
| int i3 = ip < iLen ? in[ip++] : 'A'; | |||
| if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) | |||
| throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); | |||
| int b0 = map2[i0]; | |||
| int b1 = map2[i1]; | |||
| int b2 = map2[i2]; | |||
| int b3 = map2[i3]; | |||
| if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) | |||
| throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); | |||
| int o0 = ( b0 <<2) | (b1>>>4); | |||
| int o1 = ((b1 & 0xf)<<4) | (b2>>>2); | |||
| int o2 = ((b2 & 3)<<6) | b3; | |||
| out[op++] = (byte)o0; | |||
| if (op<oLen) out[op++] = (byte)o1; | |||
| if (op<oLen) out[op++] = (byte)o2; } | |||
| return out; } | |||
| // Dummy constructor. | |||
| private Base64Coder() {} | |||
| } // end class Base64Coder | |||