1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.web;
  27 
  28 import java.lang.reflect.InvocationTargetException;
  29 import java.lang.reflect.Method;
  30 import java.lang.reflect.Modifier;
  31 import java.lang.reflect.UndeclaredThrowableException;
  32 import java.math.BigDecimal;
  33 import java.math.BigInteger;
  34 import java.util.ArrayList;
  35 import java.util.Collections;
  36 import java.util.List;
  37 
  38 import java.security.AccessController;
  39 import java.security.PrivilegedAction;
  40 import java.security.PrivilegedExceptionAction;
  41 
  42 import sun.reflect.misc.MethodUtil;
  43 import sun.reflect.misc.ReflectUtil;
  44 
  45 
  46 // Java Object exported to JavaScript
  47 class ExportedJavaObject {
  48 
  49     private final JS2JavaBridge owner;
  50     private final String objId; // id to call
  51     private final Object javaObject;
  52     private final Class cls;
  53     private Method[] methods;
  54     private List<String> jsNames = new ArrayList<String>(1);
  55 
  56     private JS2JavaBridge getJSBridge() {
  57         return owner;
  58     }
  59 
  60     public ExportedJavaObject(JS2JavaBridge owner, String objId, Object javaObject) {
  61         this.owner = owner;
  62         this.objId = objId;
  63         this.javaObject = javaObject;
  64         cls = javaObject.getClass();
  65         ReflectUtil.checkPackageAccess(cls);
  66         methods = getPublicMethods(cls);
  67     }
  68 
  69     public String getObjectId() {
  70         return objId;
  71     }
  72 
  73     public Object getJavaObject() {
  74         return javaObject;
  75     }
  76 
  77     public void addJSName(String jsName) {
  78         jsNames.add(jsName);
  79     }
  80 
  81     public List<String> getJSNames() {
  82         return Collections.unmodifiableList(jsNames);
  83     }
  84 
  85     public String getJSDecl() {
  86         StringBuilder sb = new StringBuilder();
  87         sb.append("{\n");
  88         for (int i=0; i<methods.length; i++) {
  89             if (i>0) {
  90                 sb.append(",\n");
  91             }
  92             Method m = methods[i];
  93             String methodName = m.getName();
  94             //Class<?> retClass = m.getReturnType();
  95             Class<?>[] params = m.getParameterTypes();
  96             sb.append("  ").append(methodName).append(" : function(");
  97             if (params.length > 0) {
  98                 sb.append("p0");
  99                 for (int j=1; j<params.length; j++) {
 100                     sb.append(", p").append(j);
 101                 }
 102             }
 103             sb.append(") {");
 104 
 105             sb.append(" return ").append(owner.getJavaBridge()).append(".call('").append(objId).append(":").append(i).append("', [");
 106             if (params.length > 0) {
 107                 sb.append("p0");
 108                 for (int j=1; j<params.length; j++) {
 109                     sb.append(", p").append(j);
 110                 }
 111             }
 112             sb.append("]); }");
 113         }
 114         sb.append("}");
 115         return sb.toString();
 116     }
 117 
 118     public class CallException extends Exception {
 119         public CallException(String message) {
 120             super(message);
 121         }
 122         public CallException(String message, Throwable cause) {
 123             super(message, cause);
 124         }
 125     }
 126 
 127     // Handle casting like Long to Integer, Integer to int, etc
 128     private Object cast(Class<?> desiredType, Object value) throws Exception {
 129         log("ExportedJavaObject.cast: desired=" + desiredType.getSimpleName()
 130                 + ", value=" + (value == null ? "null" : value.getClass().getSimpleName()));
 131         if (value == null) {
 132             return null;
 133         }
 134         // we need wrapper classes instead primitive
 135         if (desiredType.isPrimitive()) {
 136             Class[] wrapperClasses = new Class[]{
 137                 Byte.class, Short.class, Integer.class, Long.class,
 138                 Float.class, Double.class, Boolean.class, Character.class};
 139             for (Class wrap: wrapperClasses) {
 140                 if (desiredType.equals(wrap.getField("TYPE").get(null))) {
 141                     log("ExportedJavaObject.cast: replace " + desiredType.getName()
 142                             + " with " + wrap.getName());
 143                     desiredType = wrap;
 144                     break;
 145                 }
 146             }
 147         }
 148         if (Number.class.isAssignableFrom(value.getClass())) {
 149             Number n = (Number)value;
 150             if (desiredType.equals(Byte.class)) {
 151                 value = Byte.valueOf(n.byteValue());
 152             } else if (desiredType.equals(Short.class)) {
 153                 value = Short.valueOf(n.shortValue());
 154             } else if (desiredType.equals(Integer.class)) {
 155                 value = Integer.valueOf(n.intValue());
 156             } else if (desiredType.equals(Long.class)) {
 157                 value = Long.valueOf(n.longValue());
 158             } else if (desiredType.equals(BigInteger.class)) {
 159                 // handle BigInteger as Long
 160                 value = BigInteger.valueOf(n.longValue());
 161             } else if (desiredType.equals(Float.class)) {
 162                 value = Float.valueOf(n.floatValue());
 163             } else if (desiredType.equals(Double.class)) {
 164                 value = Double.valueOf(n.doubleValue());
 165             } else if (desiredType.equals(BigDecimal.class)) {
 166                 // handle BigDecimal as Double
 167                 value = BigDecimal.valueOf(n.doubleValue());
 168             }
 169         }
 170         return desiredType.cast(value);
 171     }
 172 
 173     // returns JSON-encoded result
 174     public String call(final String methodName, final String args) throws CallException {
 175         try {
 176             return AccessController.doPrivileged(new PrivilegedExceptionAction<String>() {
 177                 @Override
 178                 public String run() throws Exception {
 179                     return callWorker(methodName, args);
 180                 }
 181             }, getJSBridge().getAccessControlContext());
 182         } catch (Exception e) {
 183             if (e instanceof CallException) {
 184                 throw (CallException) e;
 185             }
 186 
 187             throw new UndeclaredThrowableException(e);
 188         }
 189     }
 190 
 191     private String callWorker(String methodName, String args) throws CallException {
 192         // methodName is an index in methods array
 193         Method m;
 194         try {
 195             int mIndex = Integer.parseInt(methodName);  // NumberFormatException
 196             m = methods[mIndex];    // ArrayIndexOutOfBoundsException
 197         } catch (Exception ex) {
 198             throw new CallException("Wrong method id", ex);
 199         }
 200 
 201         log("call: " + javaObject.getClass().getSimpleName()
 202                 + "." + m.getName() + "(" + args + ")");
 203 
 204         // we always encode paramaters as array
 205         Object[] params;
 206         try {
 207             Object argObj = getJSBridge().decode(args);
 208             params = (Object[])argObj;
 209         } catch (Exception ex) {
 210             throw new CallException("Could not parse arguments", ex);
 211         }
 212 
 213         Class<?>[] paramTypes = m.getParameterTypes();
 214         if (paramTypes.length != params.length) {
 215             throw new CallException("Incorrect argument number: "
 216                     + params.length + " instead " + paramTypes);
 217         }
 218 
 219         Object[] castedParams = new Object[paramTypes.length];
 220         int i = 0;
 221         try {
 222             for (i=0; i<paramTypes.length; i++) {
 223                 castedParams[i] = cast(paramTypes[i], params[i]);
 224             }
 225         } catch (Exception ex) {
 226             throw new CallException("Argument " + i + " casting error", ex);
 227         }
 228 
 229         Object resObj;
 230         try {
 231             resObj = m.invoke(javaObject, castedParams);
 232             // return null if the method is void
 233             if (Void.class.equals(m.getReturnType())) {
 234                 return null;
 235             }
 236         } catch (InvocationTargetException ex) {
 237             // the method throws ex.getTargetException()
 238             Throwable reason = ex.getTargetException();
 239             throw new CallException("Java Exception ("
 240                     + reason.getClass().getSimpleName() + "): "
 241                     + reason.getMessage(), reason);
 242         } catch (Exception ex) {
 243             // IllegalAccessException, IllegalArgumentException,
 244             // NullPointerException, ExceptionInInitializerError
 245             throw new CallException("Invoke error ("
 246                     + ex.getClass().getSimpleName() + "): "
 247                     + ex.getMessage(), ex);
 248         }
 249 
 250         try {
 251             StringBuilder sb = new StringBuilder(1024);
 252             getJSBridge().encode(resObj, sb);
 253             return sb.toString();
 254         } catch (Exception ex) {
 255             throw new CallException("Result encoding error", ex);
 256         }
 257     }
 258 
 259     private Method[] getPublicMethods(final Class clz) {
 260         Method[] m = clz.getMethods();
 261         ArrayList<Method> am = new ArrayList<Method>();
 262         for (int i = 0; i < m.length; i++) {
 263             if (Modifier.isPublic(m[i].getModifiers())){
 264                 am.add(m[i]);
 265             }
 266         }
 267 
 268         Method[] publicMethods = new Method[am.size()];
 269         return am.toArray(publicMethods);
 270     }
 271 
 272     static void log(String s) {
 273         JS2JavaBridge.log(s);
 274     }
 275 
 276     static void log(Exception ex) {
 277         JS2JavaBridge.log(ex);
 278     }
 279 
 280 }