/* * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ package com.sun.amms; import java.io.InputStream; import java.io.OutputStream; import java.util.Vector; import javax.microedition.amms.MediaProcessor; import javax.microedition.amms.MediaProcessorListener; import javax.microedition.media.Control; import javax.microedition.media.MediaException; import java.util.Hashtable; public abstract class BasicMediaProcessor implements MediaProcessor { /* * Several comments and rules: * * 1). this class implements public methods of MediaProcessor I/F. * For some of them it defines internal requestXXX() & doXXX() * methods that are to be redefined and implemented in derived classes. * Public methods, that have "internal" pair assumed to be "final" * and SHALL NOT be redefined in derived classes. * These are: start(), stop(), complete(), abort(), * setInput(), setOutput(). * * In general, a public method updates the state and invokes callback, * while its internal pair does all object-specific processing and return * true/false to indicate success/failure. Based on these return codes * public methods update state, invoke appropriate callbacks and * throw MediaException when needed. * * 2). state changes & callbacks shall be done in a synchronized section * (use "stateLock" object for that). A dedicated object instead of 'this" * is used to protect algorithm from accidental use of "this"-based * synchronization for different purposes in derived classes. * * 3). "abort()", "stop()" & "notifyCompleted()" can conflict * in resulting state and invoked callback. * */ /* Input and output parameters */ protected InputStream inputStream = null; protected int inputStreamLength; protected Object inputObject = null; protected OutputStream outputStream = null; private boolean inputWasSet = false; /* Current media processor state */ protected int state; /* Media processor controls */ private Control[] controls; private String[] controlNames; /* Media processor listeners */ private Vector listeners; private boolean listenersModified; /* Media processor synchronizers */ private Object stateLock = new Object(); private Object processorLock = new Object(); private static Object idLock = new Object(); private static int curID = 0; protected int mediaProcessorID; /* Listener for complete events */ private MPListenerWait mpWait = new MPListenerWait(); /** * hashtable to map processorsID to instances */ private static Hashtable mprocessors = new Hashtable(4); /** * Amount of work completed (0 - 100%) * * Subclasses need to set this value! */ protected int progress = UNKNOWN; protected void setControls(Control[] controls, String[] controlNames) { this.controls = controls; this.controlNames = controlNames; } /** * BasicMediaProcessorInternal I/F method * Subclasses need to implement this to perform implementation specific * checks, allocations, etc * * @param input input stream. * @param length length of input stream or UNKNOWN. * @return true on success, false otherwise * @throws MediaException if the input could not be set * and exception shall be delivered to user. */ protected abstract boolean doSetInput(InputStream input, int length) throws MediaException; /** * BasicMediaProcessorInternal I/F method * Subclasses need to implement this to perform implementation specific * checks, allocations, etc * * @return true on success, false otherwise * @throws MediaException if the input could not be set * and exception shall be delivered to user. */ protected abstract boolean doSetInput(Object image) throws MediaException; /** * BasicMediaProcessorInternal I/F method * Subclasses need to implement this to start * the <code>MediaProcessor</code>. * * @return true on success (signal to move to STARTED state) * @throws MediaException if the <code>MediaProcessor</code> * could not be started and exception shall be delivered to user. */ protected abstract boolean doStart() throws MediaException; /** * BasicMediaProcessorInternal I/F method * Subclasses need to implement this to stop * the <code>MediaProcessor</code>. * * @return true on success (MediaProcessor going to stop) * @throws MediaException if the <code>MediaProcessor</code> * could not be stopped. */ protected abstract boolean doStop() throws MediaException; /** * BasicMediaProcessorInternal I/F method * Subclasses need to implement this to continue execution * the <code>MediaProcessor</code>. * * @return true on success (MediaProcessor continued) * @throws MediaException if the <code>MediaProcessor</code> * could not be continued. */ protected abstract boolean doContinue() throws MediaException; /** * BasicMediaProcessorInternal I/F method * Writes output data to the stream. * * @return true on successful completion, false otherwise * (processing errors) */ protected abstract boolean doOutput(); /** * BasicMediaProcessorInternal I/F method * Aborts media processing. * * @return true on successful completion, */ protected abstract boolean doAbort(); /** * Public constructor. * * @param allCtrls acceptable controls lists. * */ public BasicMediaProcessor() { this.controlNames = new String[0]; this.controls = new Control[0]; mediaProcessorID = 0; synchronized (idLock) { mediaProcessorID = (curID + 1) & 32767; curID = mediaProcessorID; } mprocessors.put(new Integer(mediaProcessorID), this); listeners = new Vector(); inputStream = null; outputStream = null; state = UNREALIZED; } public Control getControl(String controlType) { if (controlType == null) throw new IllegalArgumentException("Invalid control type"); /* Currently, the specification say, that controls may be get in unrealized state, but in the proposals for JSR 234 (N20) suggested to forbid it. */ /* if (state == UNREALIZED) throw new IllegalStateException("Invalid state: " + state);*/ // Prepend the package name if the type given does not // have the package prefix. String type = (controlType.indexOf('.') < 0) ? ("javax.microedition.media.control." + controlType) : controlType; for (int i = 0; i < controlNames.length; i++) if (type.equals(controlNames[i])) return controls[i]; return null; } public Control[] getControls() { /* Currently, the specification say, that controls may be get in unrealized state, but in the proposals for JSR 234 (N20) suggested to forbid it. */ /* if (state == UNREALIZED) throw new IllegalStateException("Invalid state: " + state);*/ Control[] result = new Control[controls.length]; System.arraycopy(controls, 0, result, 0, controls.length); return result; } /** * Sets the input of the media processor. * @param input The <code>InputStream</code> to be used as input. * @param length The estimated length of the processed media in bytes. Since the input * is given as an <code>InputStream</code>, the implementation cannot find out * what is the length of the media until it has been processed. The estimated * length is used only when the <code>progress</code> method is used to query the * progress of the processing. If the length is not known, UNKNOWN should be passed * as a length. * @throws IllegalStateException if the <code>MediaProcessor</code> * was not in UNREALIZED or REALIZED state. * @throws javax.microedition.media.MediaException if input can not be given as a stream. * @throws IllegalArgumentException if input is null. * @throws IllegalArgumentException if length < 1 and length != UNKNOWN. * */ public void setInput(InputStream input, int length) throws javax.microedition.media.MediaException { inputWasSet = false; synchronized (stateLock) { if (state != UNREALIZED && state != REALIZED) { throw new IllegalStateException("Invalid state " + state); } if (null == input) { throw new IllegalArgumentException("Invalid input stream"); } if (length < 1 && length != UNKNOWN) { throw new IllegalArgumentException("Invalid input stream length " + length); } inputWasSet = doSetInput(input, length); // Reset object. Use stream. inputObject = null; inputStream = input; inputStreamLength = length; if(isRealizable()) notifyRealized(); } } /** * Sets the input of the media processor as an Image. * <p> * Setting the input as an Image allows use of raw image data in a convenient way. It also * allows converting Images to image files. * <code>image</code> is an UI Image of the implementing platform. For example, in MIDP * <code>image</code> is <code>javax.microedition.lcdui.Image</code> object. * </p> * <p> * Mutable Image is allowed as an input but the behavior is unspecified if the Image * is changed during processing. * </p> * @param image The <code>Image</code> object to be used as input. * @throws IllegalStateException if the <code>MediaProcessor</code> was not in <i>UNREALIZED</i> or <i>REALIZED</i> state. * @throws javax.microedition.media.MediaException if input can not be given as an image. * * @throws IllegalArgumentException if the image is not an Image object. * */ public synchronized void setInput(Object image) throws javax.microedition.media.MediaException { inputWasSet = false; synchronized (stateLock) { if (state != UNREALIZED && state != REALIZED) throw new IllegalStateException("Invalid state " + state); inputWasSet = doSetInput(image); // Reset stream. Use object. if (inputStream != null) { //try {inputStream.close();} catch(); inputStream = null; } inputObject = image; if(isRealizable()) notifyRealized(); } } /** * Sets the output of the media processor. * @param output The <code>OutputStream</code> to be used as output. * @throws IllegalArgumentException if output is null. * @throws IllegalStateException if the <code>MediaProcessor</code> was not in <i>UNREALIZED</i> or <i>REALIZED</i> state. */ public void setOutput(OutputStream output) { synchronized (stateLock) { if (state != UNREALIZED && state != REALIZED) { throw new IllegalStateException("Invalid state " + state); } if (output == null) { throw new IllegalArgumentException("Invalid output stream"); } outputStream = output; if(isRealizable()) { notifyRealized(); } } } /** * checks if the media processor can be transitioned * from UNREALIZED to REALIZED state. */ private boolean isRealizable() { return ( ((inputStream != null) || (inputObject != null)) && (outputStream != null) && (state == UNREALIZED)); } /** * Starts processing. If the <code>MediaProcessor</code> is in * STARTED state, the call is ignored. Upon calling this method, * the <code>MediaProcessor</code> either throws a * <code>MediaException</code> or moves to <i>STARTED</i> state and posts * <code>PROCESSING_STARTED</code> event to <code>MediaProcessorListener</code>s. * * <p>After the processing has been completed, a <code>PROCESSING_COMPLETED</code> * event will be delivered and the <code>MediaProcessor</code> * will move into the <i>UNREALIZED</i> state.</p> * @throws IllegalStateException if the <code>MediaProcessor</code> * was in <i>UNREALIZED</i> state. * @throws MediaException if the <code>MediaProcessor</code> * could not be started. */ public final void start() throws MediaException { if (state == STARTED) { return; } synchronized (stateLock) { /* do nothing if already in STARTED state */ if (state == STARTED) { return; } /* valid states are only REALIZED and STOPPED */ if (state == UNREALIZED) throw new IllegalStateException("Invalid state " + state); if (!inputWasSet) throw new MediaException("Incorrect or unsupported input data"); boolean isStarted = false; if (state == STOPPED) isStarted = doContinue(); else isStarted = doStart(); if (isStarted) { state = STARTED; /** requestStart & notification is done inside stateLock. So * in any case PROCESSING_STARTED will be send early than * PROCESSING_COMPLETED or PROCESSING_EROR */ notifyListeners(MediaProcessorListener.PROCESSING_STARTED, new Integer(getProgressInTenths())); } else throw new MediaException("Failed to start operation"); } } /** * Stops processing temporarily. If the * <code>MediaProcessor</code> is in a <i>UNREALIZED</i>, <i>REALIZED</i> or <i>STOPPED</i> state, the * call is ignored. Otherwise, the <code>MediaProcessor</code> * either throws a <code>MediaException</code> or moves to * <i>STOPPED</i> state and posts <code>PROCESSING_STOPPED</code> event to <code>MediaProcessorListener</code>s. * * @throws MediaException if the <code>MediaProcessor</code> * could not be stopped. */ public final void stop() throws MediaException { /* do nothing if not in STARTED state */ if (state != STARTED) { return; } synchronized (stateLock) { if (state != STARTED) { return; } if (doStop()) { notifyStopped(); } else { throw new MediaException("Failed to stop operation"); } } } /** * Waits until the processing has been completed. If * the <code>MediaProcessor</code> is not in <i>STARTED</i> state, calls * <code>start</code> implicitly causing <code>PROCESSING_STARTED</code> event to be posted * to <code>MediaProcessorListener</code>s. Otherwise, waits until either * a <code>MediaException</code> is thrown and <code>PROCESSING_ERROR</code> is posted * or a <code>PROCESSING_COMPLETED</code> * event has been posted. After this method returns, the * <code>MediaProcessor</code> is in <i>UNREALIZED</i> state if the processing was succesful * or in <i>REALIZED</i> state if the processing failed. * @throws IllegalStateException if the <code>MediaProcessor</code> * was in <i>UNREALIZED</i> state * @throws MediaException if completing the processing failed either because the processing couldn't be started * or because an error occured during processing. */ public final void complete() throws MediaException { /* * need to do same checks as start() method does ... * need to be inlined if there is a risk that start() method in derived * classes will be inlined ! */ synchronized(stateLock) { if (state == UNREALIZED) throw new IllegalStateException("Invalid state " + state); if (state != STARTED) /// If not started start start(); } String event = mpWait.Complete(); if (event != MediaProcessorListener.PROCESSING_COMPLETED) throw new MediaException("Media processing was interrupted: " + event); } /** * Aborts the processing even if the processing was not * complete. * Any bytes that were written to output may not be * reasonable and should be discarded. A <code>PROCESSING_ABORTED</code> * event is posted and the <code>MediaProcessor</code> is * moved into <i>UNREALIZED</i> state. * <p> * Ignored if the * <code>MediaProcessor</code> was in <i>REALIZED</i> or <i>UNREALIZED</i> state. */ public synchronized final void abort() { /* do nothing of not in STARTED or STOPPED state - nothing to abort */ if (state != STARTED && state != STOPPED) { return; } // close input & output streams before callback synchronized (stateLock) { if (state != STARTED && state != STOPPED) { return; } if (doAbort()) { synchronized (stateLock) { closeAllStreams(); state = UNREALIZED; notifyListeners(MediaProcessorListener.PROCESSING_ABORTED, new Integer(getProgressInTenths())); } } else { /// Strange state. Abort doesn't throw exceptions, so what do with /// unsuccessful result ? } } } public final void addMediaProcessorListener(MediaProcessorListener listener) { if (listener != null) { /* * Explicit "sync" is needed to raise "modified" flag. * Implicit "sync" is already inside addElement() method, * so second sync from the same thread will do nothing ... */ synchronized (listeners) { listenersModified = true; listeners.addElement(listener); } } } public final void removeMediaProcessorListener(MediaProcessorListener listener) { if (listener != null) { /* * Explicit "sync" is needed to raise "modified" flag. * Implicit "sync" is already inside removeElemet() method, * so second sync from the same thread will do nothing ... */ synchronized (listeners) { listenersModified = true; listeners.removeElement(listener); } } } /** * Get an estimated percentage of work that has been done. * * @return * <ul> * <li>0, if the <code>MediaProcessor</code> is in <i>UNREALIZED</i> or <i>REALIZED</i> state * <li>amount of work completed (0 - 100%) if <code>MediaProcessor</code> is in <i>STARTED</i> or <i>STOPPED</i> states * <li><code>UNKNOWN</code>, if the estimation cannot be calculated. * </ul> */ public int getProgress() { return ((state == UNREALIZED) || (state == REALIZED)) ? 0 : progress; } /** * Get the current MediaProcessor state. * * @return * <ul> * <li><i>UNREALIZED</i> * <li><i>REALIZED</i> * <li><i>STOPPED</i> * <li><i>STARTED</i> * </ul> */ public int getState() { return state; } /** * Called to notify about processing completed (or processing error) */ protected void notifyCompleted(boolean processingSuccess) { if (processingSuccess) { processingSuccess = doOutput(); synchronized (stateLock) { // close input & output streams before callback closeAllStreams(); state = UNREALIZED; notifyListeners(MediaProcessorListener.PROCESSING_COMPLETED, new Boolean(processingSuccess)); } } else { synchronized (stateLock) { state = REALIZED; notifyListeners(MediaProcessorListener.PROCESSING_ERROR, ""); } } } protected void notifyStopped() { synchronized (stateLock) { state = STOPPED; notifyListeners(MediaProcessorListener.PROCESSING_STOPPED, new Integer(getProgressInTenths())); } } /** * Called from setInput() & setOutput() methods * to notify that processor wants to move to REALIZED state. */ private void notifyRealized() { synchronized (stateLock) { state = REALIZED; notifyListeners(MediaProcessorListener.PROCESSOR_REALIZED, new Integer(getProgressInTenths())); } } private void notifyListeners(String message, Object obj) { Object copy[]; synchronized (listeners) { copy = new Object[listeners.size()]; listeners.copyInto(copy); listenersModified = false; } for (int i = 0; i < copy.length; i++) { MediaProcessorListener listener = (MediaProcessorListener)copy[i]; listener.mediaProcessorUpdate(this, message, obj); } mpWait.mediaProcessorUpdate(this, message, obj); /* * need to check for "listenersModified == true", * this means that one of callbacks updated listeners -> * need some actions ... */ } private int getProgressInTenths() { return ((progress > 0) && (progress <= 100) ? (progress * 10) : progress); } private boolean closeAllStreams() { boolean result; try { if (inputStream != null) inputStream.close(); if (outputStream != null) outputStream.close(); /* * Reset handles to avoid false transition to REALIZED state after * next input setup */ inputStream = null; outputStream = null; inputObject = null; result = true; } catch (java.io.IOException ioex) { result = false; } return result; } private boolean waitProcessing() { return false; } /** * For processors management * * @param pid Description of the Parameter * @return Description of the Return Value */ public static BasicMediaProcessor get(int mpid) { return (BasicMediaProcessor) (mprocessors.get(new Integer(mpid))); } } class MPListenerWait implements MediaProcessorListener{ boolean isWait = true; String event = MediaProcessorListener.PROCESSING_ERROR; public void mediaProcessorUpdate( MediaProcessor processor, String event, Object eventData ) { this.event = event; isWait = (event == MediaProcessorListener.PROCESSING_STARTED); if (!isWait) { synchronized(this) { this.notifyAll(); } } } public String Complete() { synchronized(this) { while (isWait) try { this.wait(); } catch(Exception e) {}; } return event; } }