Java tutorial
/* Copyright 2008-2014 ITACA-TSB, http://www.tsb.upv.es Instituto Tecnologico de Aplicaciones de Comunicacion Avanzadas - Grupo Tecnologias para la Salud y el Bienestar (TSB) See the NOTICE file distributed with this work for additional information regarding copyright ownership Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.universAAL.android.proxies; import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; import java.net.URLEncoder; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Random; import org.apache.http.client.utils.URLEncodedUtils; import org.universAAL.android.container.AndroidContainer; import org.universAAL.android.container.AndroidContext; import org.universAAL.android.services.MiddlewareService; import org.universAAL.android.utils.Config; import org.universAAL.android.utils.GroundingParcel; import org.universAAL.android.utils.AppConstants; import org.universAAL.android.utils.RAPIManager; import org.universAAL.android.utils.VariableSubstitution; import org.universAAL.middleware.bus.msg.BusMessage; import org.universAAL.middleware.rdf.Resource; import org.universAAL.middleware.rdf.TypeMapper; import org.universAAL.middleware.serialization.MessageContentSerializerEx; import org.universAAL.middleware.service.CallStatus; import org.universAAL.middleware.service.ServiceBus; import org.universAAL.middleware.service.ServiceCall; import org.universAAL.middleware.service.ServiceCallee; import org.universAAL.middleware.service.ServiceResponse; import org.universAAL.middleware.service.impl.ServiceRealization; import org.universAAL.middleware.service.owl.Service; import org.universAAL.middleware.service.owls.process.ProcessOutput; import org.universAAL.middleware.service.owls.profile.ServiceProfile; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; /** * Class that acts as a connection between an Android component and a universAAL * wrapper. In this case, between a service callee, an intent, and a broadcast * receiver for the response. The translation is made thanks to metadata * grounding. * * @author alfiva * */ public class ServiceCalleeProxy extends ServiceCallee { private WeakReference<Context> contextRef; private String action = null; private String category = null; private String replyAction = null; private String replyCategory = null; private String grounding = null; private Hashtable<String, String> inputURItoExtraKEY; private Hashtable<String, String> extraKEYtoOutputURI; private BroadcastReceiver receiver = null; private String spURI = null; /** * Constructor for the proxy. * * @param parcel * The parcelable from of the grounding of the metadata. * @param context * The Android context. */ public ServiceCalleeProxy(GroundingParcel parcel, Context context) { super(AndroidContext.THE_CONTEXT, prepareProfiles(parcel.getGrounding())); contextRef = new WeakReference<Context>(context); action = parcel.getAction(); category = parcel.getCategory(); replyAction = parcel.getReplyAction(); replyCategory = parcel.getReplyCategory(); grounding = prepareGrounding(parcel.getGrounding()); fillTableIN(parcel.getLengthIN(), parcel.getKeysIN(), parcel.getValuesIN()); fillTableOUT(parcel.getLengthOUT(), parcel.getKeysOUT(), parcel.getValuesOUT()); ServiceProfile[] sps = prepareProfiles(parcel.getGrounding()); spURI = sps[0].getURI(); // This is for RAPI. sync(); } public void sync() { if (MiddlewareService.isGWrequired()) { switch (Config.getRemoteType()) { case AppConstants.REMOTE_TYPE_GW: // Does not need syncing in GW, transparent break; case AppConstants.REMOTE_TYPE_RAPI: //Publish as well in the RAPI TODO What if offline!!!!!!!!????? RAPIManager.invokeInThread(RAPIManager.PROVIDES, grounding); break; default: break; } } } /** * Extract the Service Profile information from the grounding. * * @param parcel * The parcelable from of the grounding of the metadata. * @return The uAAL Service Profile. */ private static ServiceProfile[] prepareProfiles(String grounding) { MessageContentSerializerEx parser = (MessageContentSerializerEx) AndroidContainer.THE_CONTAINER .fetchSharedObject(AndroidContext.THE_CONTEXT, new Object[] { MessageContentSerializerEx.class.getName() });//TODO throw ex if error ServiceProfile sp = (ServiceProfile) parser.deserialize(grounding); String tenant = Config.getServerUSR(); try { tenant = URLEncoder.encode(tenant, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // Matchmaking uses SP URI and Process URI (which is built from Service // URI) to distinguish callees, so these must be made unique for each // tenant despite being the same profile in each of them. sp.changeURI(sp.getURI() + tenant);// Add tenant ID to SP URI Service serv = sp.getTheService(); String servURI = serv.getURI(); serv.changeURI(servURI + tenant);// Add tenant ID to Service URI sp.setProperty(Service.PROP_OWLS_PRESENTED_BY, serv); String process = sp.getProcessURI();// Process URI = Service URI + "Process" (but maybe suffix will change in future?) String suffix = process.substring(servURI.length());// Get the "Process" suffix of the Process URI sp.changeProperty(ServiceProfile.PROP_OWLS_PROFILE_HAS_PROCESS, null);// Remove first cause doesnt work well sp.setProperty(ServiceProfile.PROP_OWLS_PROFILE_HAS_PROCESS, servURI + tenant + suffix);// Add tenant ID in Process URI return new ServiceProfile[] { sp }; } /** * Modify the serialized grounding to append tenant ID to relevant URIs. * Equivalent to prepareGrounding but returns it serialized. * * @param serial * The grounding of the metadata. * @return The modified uAAL Service Profile. */ private static String prepareGrounding(String grounding) { MessageContentSerializerEx parser = (MessageContentSerializerEx) AndroidContainer.THE_CONTAINER .fetchSharedObject(AndroidContext.THE_CONTEXT, new Object[] { MessageContentSerializerEx.class.getName() });//TODO throw ex if error ServiceProfile sp = (ServiceProfile) parser.deserialize(grounding); String tenant = Config.getServerUSR(); try { tenant = URLEncoder.encode(tenant, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // Matchmaking uses SP URI and Process URI (which is built from Service // URI) to distinguish callees, so these must be made unique for each // tenant despite being the same profile in each of them. sp.changeURI(sp.getURI() + tenant);// Add tenant ID to SP URI Service serv = sp.getTheService(); String servURI = serv.getURI(); serv.changeURI(servURI + tenant);// Add tenant ID to Service URI sp.setProperty(Service.PROP_OWLS_PRESENTED_BY, serv); String process = sp.getProcessURI();// Process URI = Service URI + "Process" (but maybe suffix will change in future?) String suffix = process.substring(servURI.length());// Get the "Process" suffix of the Process URI sp.changeProperty(ServiceProfile.PROP_OWLS_PROFILE_HAS_PROCESS, null);// Remove first cause doesnt work well sp.setProperty(ServiceProfile.PROP_OWLS_PROFILE_HAS_PROCESS, servURI + tenant + suffix);// Add tenant ID in Process URI return parser.serialize(sp); } /** * Extract the table of mappings between inputs and extras from the * grounding. * * @param length * Amount of entries. * @param keys * Input keys. * @param values * Extras ID values. */ private void fillTableIN(int length, String[] keys, String[] values) { inputURItoExtraKEY = new Hashtable<String, String>(length); for (int i = 0; i < length; i++) { inputURItoExtraKEY.put(keys[i], values[i]); } } /** * Extract the table of mappings between extras and outputs from the * grounding. * * @param length * Amount of entries. * @param keys * Extras keys. * @param values * Outputs values. */ private void fillTableOUT(int length, String[] keys, String[] values) { extraKEYtoOutputURI = new Hashtable<String, String>(length); for (int i = 0; i < length; i++) { extraKEYtoOutputURI.put(keys[i], values[i]); } } @Override public void communicationChannelBroken() { // TODO Auto-generated method stub } @Override public void close() { super.close(); Context ctxt = this.contextRef.get(); if (ctxt != null && receiver != null) { ctxt.unregisterReceiver(receiver); } } @Override public void handleRequest(BusMessage m) { ServiceCall call = (ServiceCall) m.getContent(); // Extract the origin action and category from the call String fromAction = (String) call.getNonSemanticInput(AppConstants.UAAL_META_PROP_FROMACTION); String fromCategory = (String) call.getNonSemanticInput(AppConstants.UAAL_META_PROP_FROMCATEGORY); Boolean needsOuts = (Boolean) call.getNonSemanticInput(AppConstants.UAAL_META_PROP_NEEDSOUTPUTS); boolean isNonIntrusive = fromAction != null && fromAction.equals(action) && fromCategory != null && fromCategory.equals(category); // This means the serv Intent in the caller proxy is the same as the // serv Intent in destination native app. Therefore the destination will // already have received it and we must not send the intent to avoid // duplication or infinite loops. if (!isNonIntrusive) { // In this case the serv intents are different because the origin // action+cat is being used as a kind of API to call the SCaller. // In this case we have to relay the call to the destination native app. Context ctxt = contextRef.get(); if (ctxt != null) { // Prepare an intent for sending to Android grounded service Intent serv = new Intent(action); serv.addCategory(category); boolean expecting = false; // If a response is expected, prepare a callback receiver (which must be called by uaalized app) if ((replyAction != null && !replyAction.isEmpty()) && (replyCategory != null && !replyCategory.isEmpty())) { // Tell the destination where to send the reply serv.putExtra(AppConstants.ACTION_META_REPLYTOACT, replyAction); serv.putExtra(AppConstants.ACTION_META_REPLYTOCAT, replyCategory); // Register the receiver for the reply receiver = new ServiceCalleeProxyReceiver(m);// TODO Can only handle 1 call at a time per proxy IntentFilter filter = new IntentFilter(replyAction); filter.addCategory(replyCategory); ctxt.registerReceiver(receiver, filter); expecting = true; } else if (needsOuts != null && needsOuts.booleanValue()) { // No reply* fields set, but caller still needs a response, // lets build one (does not work for callers outside android MW) Random r = new Random(); String action = AppConstants.ACTION_REPLY + r.nextInt(); serv.putExtra(AppConstants.ACTION_META_REPLYTOACT, action); serv.putExtra(AppConstants.ACTION_META_REPLYTOCAT, Intent.CATEGORY_DEFAULT); // Register the receiver for the reply receiver = new ServiceCalleeProxyReceiver(m); IntentFilter filter = new IntentFilter(action); filter.addCategory(Intent.CATEGORY_DEFAULT); ctxt.registerReceiver(receiver, filter); expecting = true; } // Take the inputs from the call and put them in the intent if (inputURItoExtraKEY != null && !inputURItoExtraKEY.isEmpty()) { VariableSubstitution.putCallInputsAsIntentExtras(call, serv, inputURItoExtraKEY); } // Flag to avoid feeding back the intent to bus when intent is the same in app and in callerproxy serv.putExtra(AppConstants.ACTION_META_FROMPROXY, true); // Send the intent to Android grounded service ComponentName started = ctxt.startService(serv);//Not allowed in Android 5.0 (fall back to broadcast) if (started == null) { // No android service was there, try with broadcast. Before, here it used to send failure response ctxt.sendBroadcast(serv); // No way to know if received. If no response, bus will timeout (?) } else if (!expecting) { // There is no receiver waiting a response, send success now ServiceResponse resp = new ServiceResponse(CallStatus.succeeded); sendResponse(m, resp); } //TODO Handle timeout } } } @Override public ServiceResponse handleCall(ServiceCall call) { // Empty, we handle asynchronously in handleRequest return null; } /** * Sends the response back to the service bus. * * @param msg * The message that originated the call. * @param sr * The response to return. */ private void sendResponse(final BusMessage msg, final ServiceResponse sr) { // First, since we already have the response (good or bad), remove the receiver Context ctxt = contextRef.get(); if (ctxt != null) { if (receiver != null) { ctxt.unregisterReceiver(receiver); receiver = null; } } // Then send back the response sr.setProperty(ServiceRealization.uAAL_SERVICE_PROVIDER, new Resource(busResourceURI)); BusMessage reply = msg.createReply(sr); if (reply != null) { ((ServiceBus) theBus).brokerReply(busResourceURI, reply); } } /** * Auxiliary class representing the Broadcast Receiver registered by the * middleware where apps will send intents when they have to return a * response to uAAL. * * @author alfiva * */ public class ServiceCalleeProxyReceiver extends BroadcastReceiver { BusMessage msg; public ServiceCalleeProxyReceiver(BusMessage m) { super(); msg = m; } @Override public void onReceive(Context context, Intent intent) { ServiceResponse resp = new ServiceResponse(CallStatus.succeeded); if (extraKEYtoOutputURI != null && !extraKEYtoOutputURI.isEmpty()) { VariableSubstitution.putIntentExtrasAsResponseOutputs(intent, resp, extraKEYtoOutputURI); } sendResponse(msg, resp); } } // The following are additions for the management of calls coming from R API public String getSpURI() { return spURI; } /** * This is an auxiliary method that invokes this proxy when a service * request matched in the R API server, and the ServiceCall was sent here * through GCM. We receive a ServiceCall not a BusMessage nor a * ServiceRequest. It sends the response back to the R API rather than * through the inner bus. * * @param scall * The ServiceCall as received from R API through GCM. * @param origincall * The original ServiceCall URI as specified by the server. It is * not the same as scall.getURI() since that object is created * here in the client. */ public void handleCallFromGCM(ServiceCall scall, String origincall) { Boolean needsOuts = (Boolean) scall.getNonSemanticInput(AppConstants.UAAL_META_PROP_NEEDSOUTPUTS); Context ctxt = contextRef.get(); if (ctxt != null) { // Prepare an intent for sending to Android grounded service Intent serv = new Intent(action); serv.addCategory(category); boolean expecting = false; // If a response is expected, prepare a callback receiver (which must be called by uaalized app) if ((replyAction != null && !replyAction.isEmpty()) && (replyCategory != null && !replyCategory.isEmpty())) { // Tell the destination where to send the reply serv.putExtra(AppConstants.ACTION_META_REPLYTOACT, replyAction); serv.putExtra(AppConstants.ACTION_META_REPLYTOCAT, replyCategory); // Register the receiver for the reply receiver = new ServiceCalleeProxyReceiverGCM(origincall); IntentFilter filter = new IntentFilter(replyAction); filter.addCategory(replyCategory); ctxt.registerReceiver(receiver, filter); expecting = true; } else if (needsOuts != null && needsOuts.booleanValue()) { // No reply* fields set, but caller still needs a response, lets // build one (does not work for callers outside android MW) Random r = new Random(); String action = AppConstants.ACTION_REPLY + r.nextInt(); serv.putExtra(AppConstants.ACTION_META_REPLYTOACT, action); serv.putExtra(AppConstants.ACTION_META_REPLYTOCAT, Intent.CATEGORY_DEFAULT); // Register the receiver for the reply receiver = new ServiceCalleeProxyReceiverGCM(origincall); IntentFilter filter = new IntentFilter(action); filter.addCategory(Intent.CATEGORY_DEFAULT); ctxt.registerReceiver(receiver, filter); expecting = true; } // Take the inputs from the call and put them in the intent if (inputURItoExtraKEY != null && !inputURItoExtraKEY.isEmpty()) { VariableSubstitution.putCallInputsAsIntentExtras(scall, serv, inputURItoExtraKEY); } // Flag to avoid feeding back the intent to bus when intent is the same in app and in callerproxy serv.putExtra(AppConstants.ACTION_META_FROMPROXY, true); // Send the intent to Android grounded service ComponentName started = null; try { // HACK: In android 5.0 it is forbidden to send implicit service intents like this one started = ctxt.startService(serv); } catch (Exception e) { // Therefore if it fails, fail silently and try again with broadcast receivers started = null; } if (started == null) { // No android service was there, try with broadcast. Before, here it used to send failure response ctxt.sendBroadcast(serv); // No way to know if received. If no response, bus will timeout (?) } else if (!expecting) { // There is no receiver waiting a response, send success now ServiceResponse resp = new ServiceResponse(CallStatus.succeeded); sendResponseGCM(resp, origincall); } //TODO Handle timeout } } /** * Sends the response back to the R API. * * @param sresp * The ServiceResponse to deliver * @param callURI * The ServiceCall URI that originated this response in the server */ private void sendResponseGCM(final ServiceResponse sresp, final String callURI) { StringBuilder strb = new StringBuilder(); List outputs = sresp.getOutputs(); if (outputs != null && outputs.size() > 0) { for (Iterator iter1 = outputs.iterator(); iter1.hasNext();) { Object obj = iter1.next(); if (obj instanceof ProcessOutput) { ProcessOutput output = (ProcessOutput) obj; Object value = output.getParameterValue(); String type = ""; if (value instanceof Resource) { type = ((Resource) value).getType(); } else if (value instanceof List) { if (((List) value).get(0) instanceof Resource) { type = ((Resource) ((List) value).get(0)).getType(); } else { type = TypeMapper.getDatatypeURI(((List) value).get(0)); } } else { type = TypeMapper.getDatatypeURI(((List) value).get(0)); } strb.append(output.getURI()).append("=").append(output.getParameterValue().toString()) .append("@").append(type).append("\n"); } else if (obj instanceof List) { List outputLists = (List) obj; for (Iterator iter2 = outputLists.iterator(); iter2.hasNext();) { ProcessOutput output = (ProcessOutput) iter2.next(); Object value = output.getParameterValue(); String type = ""; if (value instanceof Resource) { type = ((Resource) value).getType(); } else if (value instanceof List) { if (((List) value).get(0) instanceof Resource) { type = ((Resource) ((List) value).get(0)).getType(); } else { type = TypeMapper.getDatatypeURI(((List) value).get(0)); } } else { type = TypeMapper.getDatatypeURI(((List) value).get(0)); } strb.append(output.getURI()).append("=").append(output.getParameterValue().toString()) .append("@").append(type).append("\n"); } } } } strb.append("status=").append(sresp.getCallStatus().toString()).append("\n"); strb.append("call=").append(callURI).append("\n"); strb.append("TURTLE").append("\n"); MessageContentSerializerEx parser = (MessageContentSerializerEx) AndroidContainer.THE_CONTAINER .fetchSharedObject(AndroidContext.THE_CONTEXT, new Object[] { MessageContentSerializerEx.class.getName() });//TODO throw ex if error strb.append(parser.serialize(sresp)); //Send callback response to server RAPIManager.invokeInThread(RAPIManager.RESPONSES, strb.toString()); } /** * Auxiliary class representing the Broadcast Receiver registered by the * middleware where apps will send intents when they have to return a * response to uAAL. This is a variation for the R API through GCM, which * will send the response through R API rather than the inner bus. * * @author alfiva * */ public class ServiceCalleeProxyReceiverGCM extends BroadcastReceiver { String callURI; public ServiceCalleeProxyReceiverGCM(String call) { super(); callURI = call; } @Override public void onReceive(Context context, Intent intent) { ServiceResponse resp = new ServiceResponse(CallStatus.succeeded); if (extraKEYtoOutputURI != null && !extraKEYtoOutputURI.isEmpty()) { VariableSubstitution.putIntentExtrasAsResponseOutputs(intent, resp, extraKEYtoOutputURI); } sendResponseGCM(resp, callURI); } } }