How could I implement disposable MBean, one that doesn't prevent resource it is monitoring from being garbage collected?
Let say I wrote dummy statistic MBean but the class it is monitoring is not singleton in the system. I would like MBean to be automatically unregistered once resource is no longer in use.
Any ideas how to achieve that?
Any existing solutions?
Thanks.
Usage
registerWeakMBean("com.company:type=connection,name=" +
getClass().getSimpleName(), connectionStatMBeanImpl, ConnectionStatMBean.class);
Implementatin
package util;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.StandardMBean;
import org.apache.log4j.Logger;
import sun.management.Agent;
public class JmxUtils {
private static Logger log = Logger.getLogger(JmxUtils.class);
private static Map<Object, String> weakMBeans = new ConcurrentHashMap<Object, String>();
static {
verifyJmxAgentStarted();
}
private static final int getAvailablePort() throws IOException {
ServerSocket s = new ServerSocket(0);
int result = s.getLocalPort();
s.close();
return result;
}
/**
* #param objName
* domain:type=value[,name=value]
* #param implementation
* #param mbeanInterface
* #see ObjectName
* #see StandardMBean
*/
public static final <I> ObjectInstance registerMBean(String objName, I implementation, Class<I> mbeanInterface) {
int counter = 0;
String uniqueSuffix = "";
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
while (true) {
try {
final ObjectName name = new ObjectName(objName + uniqueSuffix);
final StandardMBean mbean = new StandardMBean(implementation, mbeanInterface);
return mbs.registerMBean(mbean, name);
} catch (final InstanceAlreadyExistsException e) {
uniqueSuffix = "" + ++counter;
} catch (final Exception e) {
throw new Error(e);
}
}
}
/**
* Weak MBean will not prevent resource it is monitoring from been garbage collected. MBean will be automatically unregistered.
*
* #param objName
* domain:type=value[,name=value]
* #param implementation
* #param mbeanInterface
* #see ObjectName
* #see StandardMBean
* #see WeakReference
*/
public static final <I> ObjectInstance registerWeakMBean(String objName, I implementation, Class<I> mbeanInterface) {
I proxy = DisposableWeakReference.newWeakReferenceProxy(new DisposableWeakReference<I>(implementation) {
#Override
public void dispose(Object disposable) {
unregisterMBean(weakMBeans.remove(disposable));
}
}, mbeanInterface);
ObjectInstance instance = registerMBean(objName, proxy, mbeanInterface);
weakMBeans.put(proxy, instance.getObjectName().getCanonicalName());
return instance;
}
public static <T> T newJmxClient(Class<T> clazz, String objectName, String serviceUrl) {
return createJmxClient(clazz, objectName, serviceUrl, null, null);
}
public static <T> T newJmxClient(Class<T> clazz, String objectName, String serviceUrl, final String user, final String pass) {
try {
JMXServiceURL jmxServiceUrl = new JMXServiceURL(serviceUrl);
Map<String, ?> env = user == null ? null : new HashMap<String, Object>() {{
put(JMXConnector.CREDENTIALS, new String[] {user, pass});
}};
JMXConnector jmxc = JMXConnectorFactory.connect(jmxServiceUrl, env);
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
ObjectName mbeanName = new ObjectName(objectName);
return JMX.newMBeanProxy(mbsc, mbeanName, clazz, true);
} catch (IOException | MalformedObjectNameException e) {
throw new RuntimeException("Can not create client for remote JMX " + serviceUrl, e);
}
}
/**
* #param objName
* #see ObjectName
*/
public static final void unregisterMBean(String objName) {
try {
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
final ObjectName name = new ObjectName(objName);
mbs.unregisterMBean(name);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private static final void verifyJmxAgentStarted() {
try {
String port = System.getProperty("com.sun.management.jmxremote.port");
if (port == null) {
port = String.valueOf(getAvailablePort());
System.setProperty("com.sun.management.jmxremote.port", port);
System.setProperty("com.sun.management.jmxremote.ssl", "false");
System.setProperty("com.sun.management.jmxremote.authenticate", "false");
Agent.startAgent();
}
log.info(InetAddress.getLocalHost().getCanonicalHostName() + ":" + port);
} catch (Exception e) {
throw new Error(e);
}
}
}
package util;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.WeakHashMap;
/**
* Disposable weak reference calls you back when referent has been disposed. You can also create proxy to the referent to emulate direct access.
*
* <pre>
* public class Example {
* public interface I {
* // interface referent is implementing to create a proxy
* }
*
* public static final class T implements I {
* public String toString() {
* return "blah";
* }
* }
*
* private WeakReference<T> wr;
* private I wrp;
* private List<Object> list = new LinkedList<Object>();
*
* private void testWeakRef() {
* T o = new T();
* wr = new DisposableWeakReference<T>(o) {
* public void dispose(Object disposable) {
* list.remove(disposable);
* }
* };
* list.add(wr);
* wrp = DisposableWeakReference.newWeakReferenceProxy(new DisposableWeakReference<I>(o) {
* public void dispose(Object disposable) {
* list.remove(disposable);
* Example.this.wrp = null;
* }
* }, I.class);
* list.add(wrp);
* }
*
* public static void main(final String[] args) throws Exception {
* Example exmple = new Example();
* exmple.testWeakRef(); // try to replace with exact implementation
*
* System.out.println("exmple.wr.get() " + exmple.wr.get()); // blah
* System.out.println("exmple.wrp " + exmple.wrp); // blah
* System.out.println("exmple.list.contains(exmple.wr) " + exmple.list.contains(exmple.wr)); // true
* System.out.println("exmple.list.contains(exmple.wrp) " + exmple.list.contains(exmple.wrp)); // true
* System.gc();
* Thread.sleep(10);
* System.out.println("exmple.wr.get() " + exmple.wr.get()); // null
* System.out.println("exmple.wrp " + exmple.wrp); // null or exception
* System.out.println("exmple.list.contains(exmple.wr) " + exmple.list.contains(exmple.wr)); // false
* System.out.println("exmple.list.contains(exmple.wrp) " + exmple.list.contains(exmple.wrp)); // false
* }
* }
*
* <pre>
*
* #param <T> weak reference referent type
* #author Mykhaylo Adamovych
*/
#SuppressWarnings({ "rawtypes" })
public abstract class DisposableWeakReference<T> extends WeakReference<T> {
public static class DisposedException extends RuntimeException {
private static final long serialVersionUID = -1176608195614694732L;
public DisposedException() {
super();
}
public DisposedException(String message) {
super(message);
}
public DisposedException(String message, Throwable cause) {
super(message, cause);
}
public DisposedException(Throwable cause) {
super(cause);
}
}
private static class ReferenceProxy<T> implements InvocationHandler {
private final DisposableWeakReference<T> reference;
public ReferenceProxy(DisposableWeakReference<T> reference) {
this.reference = reference;
}
#Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName()))
return proxy == args[0];
else if ("hashCode".equals(method.getName()))
return hashCode();
T referent = reference.get();
if (referent == null)
throw new DisposedException("Referent has been disposed.");
return method.invoke(referent, args);
}
}
private static class WeakReferenceDisposerThread extends Thread {
WeakReferenceDisposerThread() {
super("Weak Reference Disposer");
}
#Override
public void run() {
while (true)
try {
DisposableWeakReference<?> reference = (DisposableWeakReference<?>) queue.remove();
Object disposable = reference.proxy;
if (disposable == null)
disposable = reference;
reference.dispose(disposable);
} catch (Throwable e) {
// ignore any exception while disposing
}
}
}
private static final ReferenceQueue queue = new ReferenceQueue();
static {
Thread disposer = new WeakReferenceDisposerThread();
disposer.setPriority(Thread.MAX_PRIORITY - 2);
disposer.setDaemon(true);
disposer.start();
}
/**
* You can use referent directly without {#link #get()}. Runtime exception will rise in case referent has been disposed by GC. You can use
* {#link #dispose(Object)} to deal with proxy also.
*
* #param reference
* disposable weak reference
* #param clazz
* referent interface class
* #param <T>
* referent type
* #param <I>
* referent interface to create a proxy
* #return referent proxy using weak reference
*/
public static <I> I newWeakReferenceProxy(DisposableWeakReference<I> reference, Class<I> clazz) {
I proxy = ReflectUtils.<I>newProxyInstance(new ReferenceProxy<I>(reference), clazz);
reference.proxy = proxy;
return proxy;
}
private Object proxy;
public DisposableWeakReference(T referent) {
super(referent, queue);
}
/**
* Remove this weak reference wrapper from whatever when referent has been garbage collected.
*
* #param disposable
* either this reference instance or proxy instance created by {#link #newWeakReferenceProxy(DisposableWeakReference, Class)}
* #see WeakHashMap
*/
public abstract void dispose(Object disposable);
}
Related
In Android, we can create a Bitmap for a resource image/drawable using BitmapFactory:
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.image);
How can we create a corresponding PixelMap from a resource id in HarmonyOS?
Welcome to the Community!
We can obtain PixelMap from a resource with the following method.
import ohos.app.Context;
import ohos.global.resource.NotExistException;
import ohos.global.resource.RawFileEntry;
import ohos.global.resource.Resource;
import ohos.global.resource.ResourceManager;
import ohos.global.resource.WrongTypeException;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import java.io.IOException;
import java.util.Optional;
/**
* Resources Utility
*/
public class ResUtil {
private static final String LABEL = "ResUtil";
private static final String MESSAGE = "IOException | NotExistException | WrongTypeException";
private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 123401234, "ResUtil");
private ResUtil() {
}
/**
* get the path from id.
*
* #param context the context
* #param id the id
* #return the path from id
*/
public static String getPathById(Context context, int id) {
String path = "";
if (context == null) {
return path;
}
ResourceManager manager = context.getResourceManager();
if (manager == null) {
return path;
}
try {
path = manager.getMediaPath(id);
} catch (IOException | NotExistException | WrongTypeException e) {
HiLog.error(LABEL_LOG, "%{public}s: %{public}s", new Object[]{"getPathById", MESSAGE});
}
return path;
}
/**
* check if the input string is empty.
*
* #param input string
* #return boolean value
*/
public static boolean isEmpty(String input) {
return input == null || input.length() == 0;
}
/**
* get the pixel map.
*
* #param context the context
* #param id resource id
* #return the pixel map
*/
public static Optional<PixelMap> getPixelMap(Context context, int id) {
String path = getPathById(context, id);
if (context == null || isEmpty(path)) {
return Optional.empty();
}
RawFileEntry assetManager = context.getResourceManager().getRawFileEntry(path);
ImageSource.SourceOptions options = new ImageSource.SourceOptions();
options.formatHint = "image/png";
ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
try {
Resource asset = assetManager.openRawFile();
ImageSource source = ImageSource.create(asset, options);
return Optional.ofNullable(source.createPixelmap(decodingOptions));
} catch (IOException e) {
HiLog.error(LABEL_LOG, "%{public}s: %{public}s", new Object[]{"getPixelMap", "IOException"});
}
return Optional.empty();
}
}
Maybe you can try the following code :
try {
Resource resource = getResourceManager().getResource(ResourceTable.Media_icon);
PixelMapElement pixelMapElement = new PixelMapElement(resource);
PixelMap pixelMap = pixelMapElement.getPixelMap();
} catch (IOException e) {
e.printStackTrace();
} catch (NotExistException e) {
e.printStackTrace();
}
I'm working on an Android Library and I need to remove the play services dependencies to be added by the developer who used my library. I need user location last location so I need to use reflection because the location library will not be included directly in my library.
But how to create the googleApiClient from the builder via reflection ?
Digging into reflection and Play Services produce the following script. It contain the code for getting AdvertisingId and the location.
Gist also available.
private void getLocation(Context context) {
Log.d(LOG_TAG, "getLocation");
if (context.getPackageManager().checkPermission(
Manifest.permission.ACCESS_FINE_LOCATION, context.getPackageName()) == PackageManager.PERMISSION_GRANTED ||
context.getPackageManager().checkPermission(
Manifest.permission.ACCESS_COARSE_LOCATION, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
try {
Class<?> apiClientBuilderClass = Class.forName("com.google.android.gms.common.api.GoogleApiClient$Builder");
Class<?> connectionCallbackClass = Class.forName("com.google.android.gms.common.api.GoogleApiClient$ConnectionCallbacks");
Class<?> connectionFailedCallbackClass = Class.forName("com.google.android.gms.common.api.GoogleApiClient$OnConnectionFailedListener");
Class<?> locationServicesClass = Class.forName("com.google.android.gms.location.LocationServices");
Constructor<?> constructorApiBuilder = apiClientBuilderClass.getConstructor(Context.class);
Object objectApiBuilder = constructorApiBuilder.newInstance(mContext);
// Create intance of listener ConnectionCallbacks
Class<?>[] connectionClassArray = new Class<?>[1];
connectionClassArray[0] = connectionCallbackClass;
sGoogleApiClientListener = Proxy.newProxyInstance(
connectionCallbackClass.getClassLoader(), connectionClassArray, new GoogleApiClientListener());
Method connectionMethodObject = apiClientBuilderClass.getMethod("addConnectionCallbacks", connectionCallbackClass);
connectionMethodObject.invoke(objectApiBuilder, sGoogleApiClientListener);
// Create instance of OnConnectionFailedListener listener
Class<?>[] connectionFailedClassArray = new Class<?>[1];
connectionFailedClassArray[0] = connectionFailedCallbackClass;
sGoogleApiClientFailedListener = Proxy.newProxyInstance(
connectionFailedCallbackClass.getClassLoader(), connectionFailedClassArray, new GoogleApiClientFailedListener());
Method connectionFailedMethodObject = apiClientBuilderClass.getMethod("addOnConnectionFailedListener", connectionFailedCallbackClass);
connectionFailedMethodObject.invoke(objectApiBuilder, sGoogleApiClientFailedListener);
// Add Api
Method addApiMethod = apiClientBuilderClass.getMethod("addApi", Class.forName("com.google.android.gms.common.api.Api"));
addApiMethod.invoke(objectApiBuilder, locationServicesClass.getField("API").get(null));
// Build
Method buildMethod = apiClientBuilderClass.getMethod("build");
sGoogleApiClient = buildMethod.invoke(objectApiBuilder);
// Connect
for (Method method : sGoogleApiClient.getClass().getMethods()) {
if (!"connect".equals(method.getName())) {
continue;
}
method.invoke(sGoogleApiClient);
break;
}
} catch (Exception e) {
// Location is not essential, so it's not an error
Log.v(LOG_TAG, "Exception on getLocation " + Log.getStackTraceString(e));
}
} else {
sLocation = "";
}
}
// GoogleApiClient for Locationsucceded
public void onConnected() {
Location lastLocation = null;
try {
// FusedLocationApi is a field of LocationServices, getting it.
Class<?> locationServicesClass = Class.forName("com.google.android.gms.location.LocationServices");
Class<?> locationProviderClass = Class.forName("com.google.android.gms.location.FusedLocationProviderApi");
for (Method method : locationProviderClass.getMethods()) {
if (!"getLastLocation".equals(method.getName())) {
continue;
}
Object object = locationServicesClass.getField("FusedLocationApi").get(null);
lastLocation = (Location) method.invoke(object, sGoogleApiClient);
break;
}
} catch (Exception e) {
// Location is not essential, so it's not an error
Log.v(LOG_TAG, "Exception on invoke onConnected for getLastLocation" + Log.getStackTraceString(e));
}
// Location mLastLocation = LocationServices.FusedLocationApi.getLastLocation(sGoogleApiClient);
if (lastLocation != null) {
sLocation = lastLocation.getLatitude() + "," + lastLocation.getLongitude();
Log.d(LOG_TAG, "Location received : " + sLocation);
} else {
sLocation = "";
}
newResourcesAvailable();
unregisterConnectionCallbacks();
}
// GoogleApiClient for Location failed somewhere
public void onConnectionSuspended() {
sLocation = "";
newResourcesAvailable();
unregisterConnectionCallbacks();
}
// GoogleApiClient for Location failed somewhere
public void onConnectionFailed() {
sLocation = "";
newResourcesAvailable();
unregisterConnectionCallbacks();
}
private void unregisterConnectionCallbacks(){
for (Method method : sGoogleApiClient.getClass().getMethods()) {
if (!"unregisterConnectionCallbacks".equals(method.getName())) {
continue;
}
try {
method.invoke(sGoogleApiClient, sGoogleApiClientListener);
} catch (Exception ignored) {
// Exception always occur here but cannot make it work
}
break;
}
for (Method method : sGoogleApiClient.getClass().getMethods()) {
if (!"unregisterConnectionFailedListener".equals(method.getName())) {
continue;
}
try {
method.invoke(sGoogleApiClient, sGoogleApiClientFailedListener);
} catch (Exception ignored) {
// Exception always occur here but cannot make it work
}
break;
}
}
protected class RetrieveAdvertisingId extends AsyncTask<Void, Void, String> {
#Override
protected String doInBackground(Void... params) {
String id = "";
try {
Object AdvertisingInfoObject = getAdvertisingInfoObject(mContext);
id = (String) invokeInstanceMethod(AdvertisingInfoObject, "getId", null);
} catch (Exception e) {
// Catch reflection exception and GooglePlayServicesNotAvailableException |
// GooglePlayServicesRepairableException
Log.e(LOG_TAG, "Exception while getting AdvertisingId", e);
}
return id;
}
#Override
protected void onPostExecute(String advertisingId) {
super.onPostExecute(advertisingId);
if (advertisingId != null && !advertisingId.isEmpty()) {
Log.i(LOG_TAG, "Advertising ID retrieved: " + advertisingId);
sAdvertisingId = advertisingId;
} else {
Log.e(LOG_TAG, "AdversitingId is not available");
sAdvertisingId = "";
}
AsynchronousParameterManager.this.newResourcesAvailable();
}
}
/**
* Returns the AdvertisingIdInfo object
*
* #param context the android context
* #return the advertising id information object
* #throws Exception
*/
private Object getAdvertisingInfoObject(Context context) throws Exception {
return invokeStaticMethod(
"com.google.android.gms.ads.identifier.AdvertisingIdClient",
"getAdvertisingIdInfo",
new Class[]{Context.class},
context
);
}
/**
* Invokes a static method within a class
* if it can be found on the classpath.
*
* #param className The full defined classname
* #param methodName The name of the method to invoke
* #param cArgs The args that the method can take
* #param args The args to pass to the method on invocation
* #return the result of the method invoke
* #throws Exception
*/
private Object invokeStaticMethod(String className, String methodName,
Class[] cArgs, Object... args) throws Exception {
Class classObject = Class.forName(className);
return invokeMethod(classObject, methodName, null, cArgs, args);
}
/**
* Invokes a method on a static instance
* within a class by reflection.
*
* #param instance The instance to invoke a method on
* #param methodName The name of the method to invoke
* #param cArgs The args that the method can take
* #param args The args to pass to the method on invocation
* #return the result of the method invoke
* #throws Exception
*/
private Object invokeInstanceMethod(Object instance, String methodName,
Class[] cArgs, Object... args) throws Exception {
Class classObject = instance.getClass();
return invokeMethod(classObject, methodName, instance, cArgs, args);
}
/**
* Invokes methods of a class via reflection
*
* #param classObject The class to attempt invocation on
* #param methodName The name of the method to invoke
* #param instance The object instance to invoke on
* #param cArgs The args that the method can take
* #param args The args to pass to the method on invocation
* #return the result of the method invoke
* #throws Exception
*/
private Object invokeMethod(Class classObject, String methodName, Object instance,
Class[] cArgs, Object... args) throws Exception {
Method methodObject = classObject.getMethod(methodName, cArgs);
return methodObject.invoke(instance, args);
}
private class GoogleApiClientListener implements InvocationHandler {
#Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (args != null) {
if (method.getName().equals("onConnected")) {
AsynchronousParameterManager.this.onConnected();
} else if (method.getName().equals("onConnectionSuspended") ) {
AsynchronousParameterManager.this.onConnectionSuspended();
}
} else {
return 0;
}
} catch (Exception e) {
throw new RuntimeException("unexpected invocation exception: " + e.getMessage());
}
return null;
}
}
private class GoogleApiClientFailedListener implements InvocationHandler {
#Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
if (method.getName().equals("onConnectionFailed")) {
AsynchronousParameterManager.this.onConnectionFailed();
}
return 0;
} catch (Exception e) {
throw new RuntimeException("unexpected invocation exception: " + e.getMessage());
}
}
}
Below is a short simple example of using a WatchService to keep data in sync with a file. My question is how to reliably test the code. The test fails occasionally, probably because of a race condition between the os/jvm getting the event into the watch service and the test thread polling the watch service. My desire is to keep the code simple, single threaded, and non blocking but also be testable. I strongly dislike putting sleep calls of arbitrary length into test code. I am hoping there is a better solution.
public class FileWatcher {
private final WatchService watchService;
private final Path path;
private String data;
public FileWatcher(Path path){
this.path = path;
try {
watchService = FileSystems.getDefault().newWatchService();
path.toAbsolutePath().getParent().register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
load();
}
private void load() {
try (BufferedReader br = Files.newBufferedReader(path, Charset.defaultCharset())){
data = br.readLine();
} catch (IOException ex) {
data = "";
}
}
private void update(){
WatchKey key;
while ((key=watchService.poll()) != null) {
for (WatchEvent<?> e : key.pollEvents()) {
WatchEvent<Path> event = (WatchEvent<Path>) e;
if (path.equals(event.context())){
load();
break;
}
}
key.reset();
}
}
public String getData(){
update();
return data;
}
}
And the current test
public class FileWatcherTest {
public FileWatcherTest() {
}
Path path = Paths.get("myFile.txt");
private void write(String s) throws IOException{
try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.defaultCharset())) {
bw.write(s);
}
}
#Test
public void test() throws IOException{
for (int i=0; i<100; i++){
write("hello");
FileWatcher fw = new FileWatcher(path);
Assert.assertEquals("hello", fw.getData());
write("goodbye");
Assert.assertEquals("goodbye", fw.getData());
}
}
}
This timing issue is bound to happen because of the polling happening in the watch service.
This test is not really a unit test because it is testing the actual implementation of the default file system watcher.
If I wanted to make a self-contained unit test for this class, I would first modify the FileWatcher so that it does not rely on the default file system. The way I would do this would be to inject a WatchService into the constructor instead of a FileSystem. For example...
public class FileWatcher {
private final WatchService watchService;
private final Path path;
private String data;
public FileWatcher(WatchService watchService, Path path) {
this.path = path;
try {
this.watchService = watchService;
path.toAbsolutePath().getParent().register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
load();
}
...
Passing in this dependency instead of the class getting hold of a WatchService by itself makes this class a bit more reusable in the future. For example, what if you wanted to use a different FileSystem implementation (such as an in-memory one like https://github.com/google/jimfs)?
You can now test this class by mocking the dependencies, for example...
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.spi.FileSystemProvider;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
public class FileWatcherTest {
private FileWatcher fileWatcher;
private WatchService watchService;
private Path path;
#Before
public void setup() throws Exception {
// Set up mock watch service and path
watchService = mock(WatchService.class);
path = mock(Path.class);
// Need to also set up mocks for absolute parent path...
Path absolutePath = mock(Path.class);
Path parentPath = mock(Path.class);
// Mock the path's methods...
when(path.toAbsolutePath()).thenReturn(absolutePath);
when(absolutePath.getParent()).thenReturn(parentPath);
// Mock enough of the path so that it can load the test file.
// On the first load, the loaded data will be "[INITIAL DATA]", any subsequent call it will be "[UPDATED DATA]"
// (this is probably the smellyest bit of this test...)
InputStream initialInputStream = createInputStream("[INITIAL DATA]");
InputStream updatedInputStream = createInputStream("[UPDATED DATA]");
FileSystem fileSystem = mock(FileSystem.class);
FileSystemProvider fileSystemProvider = mock(FileSystemProvider.class);
when(path.getFileSystem()).thenReturn(fileSystem);
when(fileSystem.provider()).thenReturn(fileSystemProvider);
when(fileSystemProvider.newInputStream(path)).thenReturn(initialInputStream, updatedInputStream);
// (end smelly bit)
// Create the watcher - this should load initial data immediately
fileWatcher = new FileWatcher(watchService, path);
// Verify that the watch service was registered with the parent path...
verify(parentPath).register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
#Test
public void shouldReturnCurrentStateIfNoChanges() {
// Check to see if the initial data is returned if the watch service returns null on poll...
when(watchService.poll()).thenReturn(null);
assertThat(fileWatcher.getData()).isEqualTo("[INITIAL DATA]");
}
#Test
public void shouldLoadNewStateIfFileChanged() {
// Check that the updated data is loaded when the watch service says the path we are interested in has changed on poll...
WatchKey watchKey = mock(WatchKey.class);
#SuppressWarnings("unchecked")
WatchEvent<Path> pathChangedEvent = mock(WatchEvent.class);
when(pathChangedEvent.context()).thenReturn(path);
when(watchKey.pollEvents()).thenReturn(Arrays.asList(pathChangedEvent));
when(watchService.poll()).thenReturn(watchKey, (WatchKey) null);
assertThat(fileWatcher.getData()).isEqualTo("[UPDATED DATA]");
}
#Test
public void shouldKeepCurrentStateIfADifferentPathChanged() {
// Make sure nothing happens if a different path is updated...
WatchKey watchKey = mock(WatchKey.class);
#SuppressWarnings("unchecked")
WatchEvent<Path> pathChangedEvent = mock(WatchEvent.class);
when(pathChangedEvent.context()).thenReturn(mock(Path.class));
when(watchKey.pollEvents()).thenReturn(Arrays.asList(pathChangedEvent));
when(watchService.poll()).thenReturn(watchKey, (WatchKey) null);
assertThat(fileWatcher.getData()).isEqualTo("[INITIAL DATA]");
}
private InputStream createInputStream(String string) {
return new ByteArrayInputStream(string.getBytes());
}
}
I can see why you might want a "real" test for this that does not use mocks - in which case it would not be a unit test and you might not have much choice but to sleep between checks (the JimFS v1.0 code is hard coded to poll every 5 seconds, have not looked at the poll time on the core Java FileSystem's WatchService)
Hope this helps
I created a wrapper around WatchService to clean up many issues I have with the API. It is now much more testable. I am unsure about some of the concurrency issues in PathWatchService though and I have not done thorough testing of it.
New FileWatcher:
public class FileWatcher {
private final PathWatchService pathWatchService;
private final Path path;
private String data;
public FileWatcher(PathWatchService pathWatchService, Path path) {
this.path = path;
this.pathWatchService = pathWatchService;
try {
this.pathWatchService.register(path.toAbsolutePath().getParent());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
load();
}
private void load() {
try (BufferedReader br = Files.newBufferedReader(path, Charset.defaultCharset())){
data = br.readLine();
} catch (IOException ex) {
data = "";
}
}
public void update(){
PathEvents pe;
while ((pe=pathWatchService.poll()) != null) {
for (WatchEvent we : pe.getEvents()){
if (path.equals(we.context())){
load();
return;
}
}
}
}
public String getData(){
update();
return data;
}
}
Wrapper:
public class PathWatchService implements AutoCloseable {
private final WatchService watchService;
private final BiMap<WatchKey, Path> watchKeyToPath = HashBiMap.create();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Queue<WatchKey> invalidKeys = new ConcurrentLinkedQueue<>();
/**
* Constructor.
*/
public PathWatchService() {
try {
watchService = FileSystems.getDefault().newWatchService();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Register the input path with the WatchService for all
* StandardWatchEventKinds. Registering a path which is already being
* watched has no effect.
*
* #param path
* #return
* #throws IOException
*/
public void register(Path path) throws IOException {
register(path, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
/**
* Register the input path with the WatchService for the input event kinds.
* Registering a path which is already being watched has no effect.
*
* #param path
* #param kinds
* #return
* #throws IOException
*/
public void register(Path path, WatchEvent.Kind... kinds) throws IOException {
try {
lock.writeLock().lock();
removeInvalidKeys();
WatchKey key = watchKeyToPath.inverse().get(path);
if (key == null) {
key = path.register(watchService, kinds);
watchKeyToPath.put(key, path);
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Close the WatchService.
*
* #throws IOException
*/
#Override
public void close() throws IOException {
try {
lock.writeLock().lock();
watchService.close();
watchKeyToPath.clear();
invalidKeys.clear();
} finally {
lock.writeLock().unlock();
}
}
/**
* Retrieves and removes the next PathEvents object, or returns null if none
* are present.
*
* #return
*/
public PathEvents poll() {
return keyToPathEvents(watchService.poll());
}
/**
* Return a PathEvents object from the input key.
*
* #param key
* #return
*/
private PathEvents keyToPathEvents(WatchKey key) {
if (key == null) {
return null;
}
try {
lock.readLock().lock();
Path watched = watchKeyToPath.get(key);
List<WatchEvent<Path>> events = new ArrayList<>();
for (WatchEvent e : key.pollEvents()) {
events.add((WatchEvent<Path>) e);
}
boolean isValid = key.reset();
if (isValid == false) {
invalidKeys.add(key);
}
return new PathEvents(watched, events, isValid);
} finally {
lock.readLock().unlock();
}
}
/**
* Retrieves and removes the next PathEvents object, waiting if necessary up
* to the specified wait time, returns null if none are present after the
* specified wait time.
*
* #return
*/
public PathEvents poll(long timeout, TimeUnit unit) throws InterruptedException {
return keyToPathEvents(watchService.poll(timeout, unit));
}
/**
* Retrieves and removes the next PathEvents object, waiting if none are yet
* present.
*
* #return
*/
public PathEvents take() throws InterruptedException {
return keyToPathEvents(watchService.take());
}
/**
* Get all paths currently being watched. Any paths which were watched but
* have invalid keys are not returned.
*
* #return
*/
public Set<Path> getWatchedPaths() {
try {
lock.readLock().lock();
Set<Path> paths = new HashSet<>(watchKeyToPath.inverse().keySet());
WatchKey key;
while ((key = invalidKeys.poll()) != null) {
paths.remove(watchKeyToPath.get(key));
}
return paths;
} finally {
lock.readLock().unlock();
}
}
/**
* Cancel watching the specified path. Cancelling a path which is not being
* watched has no effect.
*
* #param path
*/
public void cancel(Path path) {
try {
lock.writeLock().lock();
removeInvalidKeys();
WatchKey key = watchKeyToPath.inverse().remove(path);
if (key != null) {
key.cancel();
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Removes any invalid keys from internal data structures. Note this
* operation is also performed during register and cancel calls.
*/
public void cleanUp() {
try {
lock.writeLock().lock();
removeInvalidKeys();
} finally {
lock.writeLock().unlock();
}
}
/**
* Clean up method to remove invalid keys, must be called from inside an
* acquired write lock.
*/
private void removeInvalidKeys() {
WatchKey key;
while ((key = invalidKeys.poll()) != null) {
watchKeyToPath.remove(key);
}
}
}
Data class:
public class PathEvents {
private final Path watched;
private final ImmutableList<WatchEvent<Path>> events;
private final boolean isValid;
/**
* Constructor.
*
* #param watched
* #param events
* #param isValid
*/
public PathEvents(Path watched, List<WatchEvent<Path>> events, boolean isValid) {
this.watched = watched;
this.events = ImmutableList.copyOf(events);
this.isValid = isValid;
}
/**
* Return an immutable list of WatchEvent's.
* #return
*/
public List<WatchEvent<Path>> getEvents() {
return events;
}
/**
* True if the watched path is valid.
* #return
*/
public boolean isIsValid() {
return isValid;
}
/**
* Return the path being watched in which these events occurred.
*
* #return
*/
public Path getWatched() {
return watched;
}
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PathEvents other = (PathEvents) obj;
if (!Objects.equals(this.watched, other.watched)) {
return false;
}
if (!Objects.equals(this.events, other.events)) {
return false;
}
if (this.isValid != other.isValid) {
return false;
}
return true;
}
#Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Objects.hashCode(this.watched);
hash = 71 * hash + Objects.hashCode(this.events);
hash = 71 * hash + (this.isValid ? 1 : 0);
return hash;
}
#Override
public String toString() {
return "PathEvents{" + "watched=" + watched + ", events=" + events + ", isValid=" + isValid + '}';
}
}
And finally the test, note this is not a complete unit test but demonstrates the way to write tests for this situation.
public class FileWatcherTest {
public FileWatcherTest() {
}
Path path = Paths.get("myFile.txt");
Path parent = path.toAbsolutePath().getParent();
private void write(String s) throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.defaultCharset())) {
bw.write(s);
}
}
#Test
public void test() throws IOException, InterruptedException{
write("hello");
PathWatchService real = new PathWatchService();
real.register(parent);
PathWatchService mock = mock(PathWatchService.class);
FileWatcher fileWatcher = new FileWatcher(mock, path);
verify(mock).register(parent);
Assert.assertEquals("hello", fileWatcher.getData());
write("goodbye");
PathEvents pe = real.poll(10, TimeUnit.SECONDS);
if (pe == null){
Assert.fail("Should have an event for writing good bye");
}
when(mock.poll()).thenReturn(pe).thenReturn(null);
Assert.assertEquals("goodbye", fileWatcher.getData());
}
}
I'm trying to implement an application to send, receive and parse USSD codes on android. So far i used the code on http://commandus.com/blog/?p=58 in order to get this functionality. In order for the service to work, the phone needs to be restarted. This wouldn't be a problem the first time the user installs the application, but what i've noticed while testing on the emulator is that the phone will require a restart on every update, even if there's nothing new with the service.
What i would like to know is the following:
Might there be away to make the PhoneUtils bind to my service without a restart? At least on update time?
In case there's no way to do so, i'm thinking of creating 2 applications, one that is the normal application the user would interact with and a separate one containing my service. In this case the user will be prompted on first run, in case he/she requires any of the ussd features, to install the second application. I'm worried though that this is annoying for the user. What do you think would be the best way to tackle such approach?
For reference, i've included the code i used for the service as the interface part is different than the one posted on the original website.
Interface:
* This file is auto-generated. DO NOT MODIFY.
package com.android.internal.telephony;
/**
* Interface used to interact with extended MMI/USSD network service.
*/
public interface IExtendedNetworkService extends android.os.IInterface {
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements
com.android.internal.telephony.IExtendedNetworkService {
private static final java.lang.String DESCRIPTOR = "com.android.internal.telephony.IExtendedNetworkService";
/** Construct the stub at attach it to the interface. */
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an
* com.android.internal.telephony.IExtendedNetworkService interface,
* generating a proxy if needed.
*/
public static com.android.internal.telephony.IExtendedNetworkService asInterface(
android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = (android.os.IInterface) obj
.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.android.internal.telephony.IExtendedNetworkService))) {
return ((com.android.internal.telephony.IExtendedNetworkService) iin);
}
return new com.android.internal.telephony.IExtendedNetworkService.Stub.Proxy(
obj);
}
public android.os.IBinder asBinder() {
return this;
}
#Override
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_setMmiString: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
this.setMmiString(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_getMmiRunningText: {
data.enforceInterface(DESCRIPTOR);
java.lang.CharSequence _result = this.getMmiRunningText();
reply.writeNoException();
if ((_result != null)) {
reply.writeInt(1);
android.text.TextUtils
.writeToParcel(
_result,
reply,
android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getUserMessage: {
data.enforceInterface(DESCRIPTOR);
java.lang.CharSequence _arg0;
if ((0 != data.readInt())) {
_arg0 = android.text.TextUtils.CHAR_SEQUENCE_CREATOR
.createFromParcel(data);
} else {
_arg0 = null;
}
java.lang.CharSequence _result = this.getUserMessage(_arg0);
reply.writeNoException();
if ((_result != null)) {
reply.writeInt(1);
android.text.TextUtils
.writeToParcel(
_result,
reply,
android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_clearMmiString: {
data.enforceInterface(DESCRIPTOR);
this.clearMmiString();
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements
com.android.internal.telephony.IExtendedNetworkService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
/**
* Set a MMI/USSD command to ExtendedNetworkService for further
* process. This should be called when a MMI command is placed from
* panel.
*
* #param number
* the dialed MMI/USSD number.
*/
public void setMmiString(java.lang.String number)
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(number);
mRemote.transact(Stub.TRANSACTION_setMmiString, _data,
_reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
/**
* return the specific string which is used to prompt MMI/USSD is
* running
*/
public java.lang.CharSequence getMmiRunningText()
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.lang.CharSequence _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getMmiRunningText, _data,
_reply, 0);
_reply.readException();
if ((0 != _reply.readInt())) {
_result = android.text.TextUtils.CHAR_SEQUENCE_CREATOR
.createFromParcel(_reply);
} else {
_result = null;
}
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* Get specific message which should be displayed on pop-up dialog.
*
* #param text
* original MMI/USSD message response from framework
* #return specific user message correspond to text. null stands for
* no pop-up dialog need to show.
*/
public java.lang.CharSequence getUserMessage(
java.lang.CharSequence text)
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.lang.CharSequence _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((text != null)) {
_data.writeInt(1);
android.text.TextUtils.writeToParcel(text, _data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_getUserMessage, _data,
_reply, 0);
_reply.readException();
if ((0 != _reply.readInt())) {
_result = android.text.TextUtils.CHAR_SEQUENCE_CREATOR
.createFromParcel(_reply);
} else {
_result = null;
}
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* Clear pre-set MMI/USSD command. This should be called when user
* cancel a pre-dialed MMI command.
*/
public void clearMmiString() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_clearMmiString, _data,
_reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_setMmiString = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getMmiRunningText = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_getUserMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_clearMmiString = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
}
/**
* Set a MMI/USSD command to ExtendedNetworkService for further process.
* This should be called when a MMI command is placed from panel.
*
* #param number
* the dialed MMI/USSD number.
*/
public void setMmiString(java.lang.String number)
throws android.os.RemoteException;
/**
* return the specific string which is used to prompt MMI/USSD is running
*/
public java.lang.CharSequence getMmiRunningText()
throws android.os.RemoteException;
/**
* Get specific message which should be displayed on pop-up dialog.
*
* #param text
* original MMI/USSD message response from framework
* #return specific user message correspond to text. null stands for no
* pop-up dialog need to show.
*/
public java.lang.CharSequence getUserMessage(java.lang.CharSequence text)
throws android.os.RemoteException;
/**
* Clear pre-set MMI/USSD command. This should be called when user cancel a
* pre-dialed MMI command.
*/
public void clearMmiString() throws android.os.RemoteException;
}
Service:
package net.g_el.mobile.mtc;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.IBinder;
import android.os.PatternMatcher;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.telephony.IExtendedNetworkService;
/**
* Service implements IExtendedNetworkService interface.
* USSDDumbExtendedNetworkService Service must have name
* "com.android.ussd.IExtendedNetworkService" of the intent declared in the
* Android manifest file so com.android.phone.PhoneUtils class bind to this
* service after system rebooted. Please note service is loaded after system
* reboot! Your application must check is system rebooted.
*
* #see Util#syslogHasLine(String, String, String, boolean)
*/
public class USSDDumbExtendedNetworkService extends Service {
public static final String TAG = "MobileServices";
public static final String LOG_STAMP = "*USSDTestExtendedNetworkService bind successfully*";
public static final String URI_SCHEME = "ussd";
public static final String URI_AUTHORITY = "g_el.net";
public static final String URI_PATH = "/";
public static final String URI_PAR = "return";
public static final String URI_PARON = "on";
public static final String URI_PAROFF = "off";
public static final String MAGIC_ON = ":ON;)";
public static final String MAGIC_OFF = ":OFF;(";
public static final String MAGIC_RETVAL = ":RETVAL;(";
private static boolean mActive = false;
private static CharSequence mRetVal = null;
private Context mContext = null;
private String msgUssdRunning = "G Testing...";
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_INSERT.equals(intent.getAction())) {
mContext = context;
if (mContext != null) {
msgUssdRunning = mContext.getString(R.string.msgRunning);
mActive = true;
Log.d(TAG, "activate");
}
} else if (Intent.ACTION_DELETE.equals(intent.getAction())) {
mContext = null;
mActive = false;
Log.d(TAG, "deactivate");
}
}
};
private final IExtendedNetworkService.Stub mBinder = new IExtendedNetworkService.Stub() {
#Override
public void setMmiString(String number) throws RemoteException {
Log.d(TAG, "setMmiString: " + number);
}
#Override
public CharSequence getMmiRunningText() throws RemoteException {
Log.d(TAG, "getMmiRunningText: " + msgUssdRunning);
return msgUssdRunning;
}
#Override
public CharSequence getUserMessage(CharSequence text)
throws RemoteException {
if (MAGIC_ON.contentEquals(text)) {
mActive = true;
Log.d(TAG, "control: ON");
return text;
} else {
if (MAGIC_OFF.contentEquals(text)) {
mActive = false;
Log.d(TAG, "control: OFF");
return text;
} else {
if (MAGIC_RETVAL.contentEquals(text)) {
mActive = false;
Log.d(TAG, "control: return");
return mRetVal;
}
}
}
if (!mActive) {
Log.d(TAG, "getUserMessage deactivated: " + text);
//return null;//Use this in order to cancel the output on the screen.
return text;
}
String s = text.toString();
// store s to the !
Uri uri = new Uri.Builder().scheme(URI_SCHEME).authority(URI_AUTHORITY).path(URI_PATH).appendQueryParameter(URI_PAR,text.toString()).build();
sendBroadcast(new Intent(Intent.ACTION_GET_CONTENT, uri));
mActive = false;
mRetVal = text;
Log.d(TAG, "getUserMessage: " + text + "=" + s);
return null;
}
#Override
public void clearMmiString() throws RemoteException {
Log.d(TAG, "clearMmiString");
}
};
/**
* Put stamp to the system log when PhoneUtils bind to the service after
* Android has rebooted. Application must call
* {#link Util#syslogHasLine(String, String, String, boolean)} to check is
* phone rebooted or no. Without reboot phone application does not bind tom
* this service!
*/
#Override
public IBinder onBind(Intent intent) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_INSERT);
filter.addAction(Intent.ACTION_DELETE);
filter.addDataScheme(URI_SCHEME);
filter.addDataAuthority(URI_AUTHORITY, null);
filter.addDataPath(URI_PATH, PatternMatcher.PATTERN_LITERAL);
registerReceiver(mReceiver, filter);
// Do not localize!
Log.i(TAG, LOG_STAMP);
return mBinder;
}
public IBinder asBinder() {
Log.d(TAG, "asBinder");
return mBinder;
}
#Override
public boolean onUnbind(Intent intent) {
unregisterReceiver(mReceiver);
return super.onUnbind(intent);
}
}
Thank you
Please be clear, you are completely off into the weeds of internal implementations of the platform. It sounds like you are wanting this to work "well". What you are doing is not going to work well, period. You have no guarantee that these internal implementations are going to stay the same across different versions of the platforms, or even different builds of the platform on manufacturer devices, nor that the behavior you are seeing around this interface will work the same in different situations.
This is just bad bad bad. Don't do this.
I'm working on legacy code and need to make a patch.
The problem: an ancient application sends bad HTTP POST requests. One of the parameters is not URL encoded. I know that this parameter always comes last and I know it's name. I'm now trying to fix it on the server side which is running inside tomcat.
This parameter is not accessible via standard getParameter method of HttpServletRequest, since it's malformed. Method simply returns null. But when I manually read the whole body of request through ServletInputStream all the other parameters disappear. Looks like underlying classes can't parse contents of ServletInputStream since it's drained out.
So far I've managed to make a wrapper that reads all parameters from body and overrides all parameter access methods. But if any filter in the chain before mine will try to access any parameter, everything will break since ServletInputStream will be empty.
Can I somehow evade this problem? May be there's different approach?
To summarize, If I'll read raw request body in the filter, parameters will disappear from the request. If I read single parameter, ServletInputStream will become empty and manual processing will be impossible. Moreover, it's impossible to read malformed parameter via getParameter method.
Solution I've found:
It's not enough to just redefine parameter accessing methods. Several things must be done.
A filter is needed where request will be wrapped.
A custom HttpRequestWrapper is needed with all parameter access methods overridden. Request body should be parsed in constructor and stored as a field.
getInputStream and getReader methods should be redefined as well. They return values depend on the stored request body.
Custom class extending ServletInputStream is required since this one is abstract.
This 4 combined will allow you to use getParameter without interference with getInputStream and getReader methods.
Mind that manual request parameter parsing may get complicated with multipart requests. But that's another topic.
To clarify, I redefined parameter accessing methods because my request was damaged as stated in the question. You may not need that.
Rather than overriding methods, why don't you install a servlet filter which rewrites the request?
Jason Hunter has a pretty good article on filters.
I did a more complete wrapper that allows you to still access the content in the case Content-Type is application/x-www-form-urlencoded and you already called one of the getParameterXXX methods:
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* This class implements the Wrapper or Decorator pattern.<br/>
* Methods default to calling through to the wrapped request object,
* except the ones that read the request's content (parameters, stream or reader).
* <p>
* This class provides a buffered content reading that allows the methods
* {#link #getReader()}, {#link #getInputStream()} and any of the getParameterXXX to be called
* safely and repeatedly with the same results.
* <p>
* This class is intended to wrap relatively small HttpServletRequest instances.
*
* #author pgurov
*/
public class HttpServletRequestWrapper implements HttpServletRequest {
private class ServletInputStreamWrapper extends ServletInputStream {
private byte[] data;
private int idx = 0;
ServletInputStreamWrapper(byte[] data) {
if(data == null)
data = new byte[0];
this.data = data;
}
#Override
public int read() throws IOException {
if(idx == data.length)
return -1;
return data[idx++];
}
}
private HttpServletRequest req;
private byte[] contentData;
private HashMap<String, String[]> parameters;
public HttpServletRequestWrapper() {
//a trick for Groovy
throw new IllegalArgumentException("Please use HttpServletRequestWrapper(HttpServletRequest request) constructor!");
}
private HttpServletRequestWrapper(HttpServletRequest request, byte[] contentData, HashMap<String, String[]> parameters) {
req = request;
this.contentData = contentData;
this.parameters = parameters;
}
public HttpServletRequestWrapper(HttpServletRequest request) {
if(request == null)
throw new IllegalArgumentException("The HttpServletRequest is null!");
req = request;
}
/**
* Returns the wrapped HttpServletRequest.
* Using the getParameterXXX(), getInputStream() or getReader() methods may interfere
* with this class operation.
*
* #return
* The wrapped HttpServletRequest.
*/
public HttpServletRequest getRequest() {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
return new HttpServletRequestWrapper(req, contentData, parameters);
}
/**
* This method is safe to use multiple times.
* Changing the returned array will not interfere with this class operation.
*
* #return
* The cloned content data.
*/
public byte[] getContentData() {
return contentData.clone();
}
/**
* This method is safe to use multiple times.
* Changing the returned map or the array of any of the map's values will not
* interfere with this class operation.
*
* #return
* The clonned parameters map.
*/
public HashMap<String, String[]> getParameters() {
HashMap<String, String[]> map = new HashMap<String, String[]>(parameters.size() * 2);
for(String key : parameters.keySet()) {
map.put(key, parameters.get(key).clone());
}
return map;
}
private void parseRequest() throws IOException {
if(contentData != null)
return; //already parsed
byte[] data = new byte[req.getContentLength()];
int len = 0, totalLen = 0;
InputStream is = req.getInputStream();
while(totalLen < data.length) {
totalLen += (len = is.read(data, totalLen, data.length - totalLen));
if(len < 1)
throw new IOException("Cannot read more than " + totalLen + (totalLen == 1 ? " byte!" : " bytes!"));
}
contentData = data;
String enc = req.getCharacterEncoding();
if(enc == null)
enc = "UTF-8";
String s = new String(data, enc), name, value;
StringTokenizer st = new StringTokenizer(s, "&");
int i;
HashMap<String, LinkedList<String>> mapA = new HashMap<String, LinkedList<String>>(data.length * 2);
LinkedList<String> list;
boolean decode = req.getContentType() != null && req.getContentType().equals("application/x-www-form-urlencoded");
while(st.hasMoreTokens()) {
s = st.nextToken();
i = s.indexOf("=");
if(i > 0 && s.length() > i + 1) {
name = s.substring(0, i);
value = s.substring(i+1);
if(decode) {
try {
name = URLDecoder.decode(name, "UTF-8");
} catch(Exception e) {}
try {
value = URLDecoder.decode(value, "UTF-8");
} catch(Exception e) {}
}
list = mapA.get(name);
if(list == null) {
list = new LinkedList<String>();
mapA.put(name, list);
}
list.add(value);
}
}
HashMap<String, String[]> map = new HashMap<String, String[]>(mapA.size() * 2);
for(String key : mapA.keySet()) {
list = mapA.get(key);
map.put(key, list.toArray(new String[list.size()]));
}
parameters = map;
}
/**
* This method is safe to call multiple times.
* Calling it will not interfere with getParameterXXX() or getReader().
* Every time a new ServletInputStream is returned that reads data from the begining.
*
* #return
* A new ServletInputStream.
*/
public ServletInputStream getInputStream() throws IOException {
parseRequest();
return new ServletInputStreamWrapper(contentData);
}
/**
* This method is safe to call multiple times.
* Calling it will not interfere with getParameterXXX() or getInputStream().
* Every time a new BufferedReader is returned that reads data from the begining.
*
* #return
* A new BufferedReader with the wrapped request's character encoding (or UTF-8 if null).
*/
public BufferedReader getReader() throws IOException {
parseRequest();
String enc = req.getCharacterEncoding();
if(enc == null)
enc = "UTF-8";
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(contentData), enc));
}
/**
* This method is safe to execute multiple times.
*
* #see javax.servlet.ServletRequest#getParameter(java.lang.String)
*/
public String getParameter(String name) {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
String[] values = parameters.get(name);
if(values == null || values.length == 0)
return null;
return values[0];
}
/**
* This method is safe.
*
* #see {#link #getParameters()}
* #see javax.servlet.ServletRequest#getParameterMap()
*/
#SuppressWarnings("unchecked")
public Map getParameterMap() {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
return getParameters();
}
/**
* This method is safe to execute multiple times.
*
* #see javax.servlet.ServletRequest#getParameterNames()
*/
#SuppressWarnings("unchecked")
public Enumeration getParameterNames() {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
return new Enumeration<String>() {
private String[] arr = getParameters().keySet().toArray(new String[0]);
private int idx = 0;
public boolean hasMoreElements() {
return idx < arr.length;
}
public String nextElement() {
return arr[idx++];
}
};
}
/**
* This method is safe to execute multiple times.
* Changing the returned array will not interfere with this class operation.
*
* #see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
*/
public String[] getParameterValues(String name) {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
String[] arr = parameters.get(name);
if(arr == null)
return null;
return arr.clone();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getAuthType()
*/
public String getAuthType() {
return req.getAuthType();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getContextPath()
*/
public String getContextPath() {
return req.getContextPath();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getCookies()
*/
public Cookie[] getCookies() {
return req.getCookies();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getDateHeader(java.lang.String)
*/
public long getDateHeader(String name) {
return req.getDateHeader(name);
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getHeader(java.lang.String)
*/
public String getHeader(String name) {
return req.getHeader(name);
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getHeaderNames()
*/
#SuppressWarnings("unchecked")
public Enumeration getHeaderNames() {
return req.getHeaderNames();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getHeaders(java.lang.String)
*/
#SuppressWarnings("unchecked")
public Enumeration getHeaders(String name) {
return req.getHeaders(name);
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getIntHeader(java.lang.String)
*/
public int getIntHeader(String name) {
return req.getIntHeader(name);
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getMethod()
*/
public String getMethod() {
return req.getMethod();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getPathInfo()
*/
public String getPathInfo() {
return req.getPathInfo();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getPathTranslated()
*/
public String getPathTranslated() {
return req.getPathTranslated();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getQueryString()
*/
public String getQueryString() {
return req.getQueryString();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getRemoteUser()
*/
public String getRemoteUser() {
return req.getRemoteUser();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getRequestURI()
*/
public String getRequestURI() {
return req.getRequestURI();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getRequestURL()
*/
public StringBuffer getRequestURL() {
return req.getRequestURL();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getRequestedSessionId()
*/
public String getRequestedSessionId() {
return req.getRequestedSessionId();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getServletPath()
*/
public String getServletPath() {
return req.getServletPath();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getSession()
*/
public HttpSession getSession() {
return req.getSession();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getSession(boolean)
*/
public HttpSession getSession(boolean create) {
return req.getSession(create);
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#getUserPrincipal()
*/
public Principal getUserPrincipal() {
return req.getUserPrincipal();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()
*/
public boolean isRequestedSessionIdFromCookie() {
return req.isRequestedSessionIdFromCookie();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromURL()
*/
public boolean isRequestedSessionIdFromURL() {
return req.isRequestedSessionIdFromURL();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromUrl()
*/
#SuppressWarnings("deprecation")
public boolean isRequestedSessionIdFromUrl() {
return req.isRequestedSessionIdFromUrl();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#isRequestedSessionIdValid()
*/
public boolean isRequestedSessionIdValid() {
return req.isRequestedSessionIdValid();
}
/* (non-Javadoc)
* #see javax.servlet.http.HttpServletRequest#isUserInRole(java.lang.String)
*/
public boolean isUserInRole(String role) {
return req.isUserInRole(role);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getAttribute(java.lang.String)
*/
public Object getAttribute(String name) {
return req.getAttribute(name);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getAttributeNames()
*/
#SuppressWarnings("unchecked")
public Enumeration getAttributeNames() {
return req.getAttributeNames();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getCharacterEncoding()
*/
public String getCharacterEncoding() {
return req.getCharacterEncoding();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getContentLength()
*/
public int getContentLength() {
return req.getContentLength();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getContentType()
*/
public String getContentType() {
return req.getContentType();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getLocalAddr()
*/
public String getLocalAddr() {
return req.getLocalAddr();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getLocalName()
*/
public String getLocalName() {
return req.getLocalName();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getLocalPort()
*/
public int getLocalPort() {
return req.getLocalPort();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getLocale()
*/
public Locale getLocale() {
return req.getLocale();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getLocales()
*/
#SuppressWarnings("unchecked")
public Enumeration getLocales() {
return req.getLocales();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getProtocol()
*/
public String getProtocol() {
return req.getProtocol();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getRealPath(java.lang.String)
*/
#SuppressWarnings("deprecation")
public String getRealPath(String path) {
return req.getRealPath(path);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getRemoteAddr()
*/
public String getRemoteAddr() {
return req.getRemoteAddr();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getRemoteHost()
*/
public String getRemoteHost() {
return req.getRemoteHost();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getRemotePort()
*/
public int getRemotePort() {
return req.getRemotePort();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getRequestDispatcher(java.lang.String)
*/
public RequestDispatcher getRequestDispatcher(String path) {
return req.getRequestDispatcher(path);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getScheme()
*/
public String getScheme() {
return req.getScheme();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getServerName()
*/
public String getServerName() {
return req.getServerName();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#getServerPort()
*/
public int getServerPort() {
return req.getServerPort();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#isSecure()
*/
public boolean isSecure() {
return req.isSecure();
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#removeAttribute(java.lang.String)
*/
public void removeAttribute(String name) {
req.removeAttribute(name);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#setAttribute(java.lang.String, java.lang.Object)
*/
public void setAttribute(String name, Object value) {
req.setAttribute(name, value);
}
/* (non-Javadoc)
* #see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
*/
public void setCharacterEncoding(String env)
throws UnsupportedEncodingException {
req.setCharacterEncoding(env);
}
}
I wanted to post this as a comment, but I do not have enough rep. Your solution is insufficient in that ServletInputStreamWrapper will return negative integers. For instance, mock a request with input encoding UTF-16, either big or little endian. The input may start with the Byte Order Mark indicating endianess, and when testing my statement please construct the mock request content to do so. http://en.wikipedia.org/wiki/Byte_order_mark#UTF-16 Either of these BOMs contains a 0xFF byte. Since java has no unsigned byte, this 0xFF is returned as a -1. To work around this, just change the read function like so
public int read() throws IOException {
if (index == data.length) {
return -1;
}
return data[index++] & 0xff;
}
I somewhat like your solution because it works well with Spring. At first I tried to eliminate some of the delegation code you wrote by extending from HttpServletRequestWrapper. However, Spring does something interesting: when it encounters a request of type ServletRequestWrapper it unwraps it, calling getRequest(). Problem being that my getRequest() method, as copied from your code, returns a new class that extends from HttpServletRequestWrapper... rinse and repeat infinitely. So it's sad to say, chalk up a win for not using interfaces!
You could write your own Servlet Filter and hopefully ensure that it appears first in the chain. Then wrap the ServletRequest object in something that will handle the re-writing where needed. Have a look at the Programming Customized Requests and Responses section of http://java.sun.com/products/servlet/Filters.html
------ Update ------
I must be missing something. You say you can read the request body and read the parameters yourself. Couldn't you then ensure your filter is first, wrap the ServletRequest object, read, process and store the parameters, pass your request object up the chain and offer the parameters you stored instead of the original ones?