1 /*
   2  * Copyright (c) 2012, 2018, 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 com.sun.javafx.reflect.ReflectUtil;
  43 
  44 
  45 // Java Object exported to JavaScript
  46 class ExportedJavaObject {
  47 
  48     private final JS2JavaBridge owner;
  49     private final String objId; // id to call
  50     private final Object javaObject;
  51     private final Class cls;
  52     private Method[] methods;
  53     private List<String> jsNames = new ArrayList<String>(1);
  54 
  55     private JS2JavaBridge getJSBridge() {
  56         return owner;
  57     }
  58 
  59     public ExportedJavaObject(JS2JavaBridge owner, String objId, Object javaObject) {
  60         this.owner = owner;
  61         this.objId = objId;
  62         this.javaObject = javaObject;
  63         cls = javaObject.getClass();
  64         ReflectUtil.checkPackageAccess(cls);
  65         methods = getPublicMethods(cls);
  66     }
  67 
  68     public String getObjectId() {
  69         return objId;
  70     }
  71 
  72     public Object getJavaObject() {
  73         return javaObject;
  74     }
  75 
  76     public void addJSName(String jsName) {
  77         jsNames.add(jsName);
  78     }
  79 
  80     public List<String> getJSNames() {
  81         return Collections.unmodifiableList(jsNames);
  82     }
  83 
  84     public String getJSDecl() {
  85         StringBuilder sb = new StringBuilder();
  86         sb.append("{\n");
  87         for (int i=0; i<methods.length; i++) {
  88             if (i>0) {
  89                 sb.append(",\n");
  90             }
  91             Method m = methods[i];
  92             String methodName = m.getName();
  93             //Class<?> retClass = m.getReturnType();
  94             Class<?>[] params = m.getParameterTypes();
  95             sb.append("  ").append(methodName).append(" : function(");
  96             if (params.length > 0) {
  97                 sb.append("p0");
  98                 for (int j=1; j<params.length; j++) {
  99                     sb.append(", p").append(j);
 100                 }
 101             }
 102             sb.append(") {");
 103 
 104             sb.append(" return ").append(owner.getJavaBridge()).append(".call('").append(objId).append(":").append(i).append("', [");
 105             if (params.length > 0) {
 106                 sb.append("p0");
 107                 for (int j=1; j<params.length; j++) {
 108                     sb.append(", p").append(j);
 109                 }
 110             }
 111             sb.append("]); }");
 112         }
 113         sb.append("}");
 114         return sb.toString();
 115     }
 116 
 117     public class CallException extends Exception {
 118         public CallException(String message) {
 119             super(message);
 120         }
 121         public CallException(String message, Throwable cause) {
 122             super(message, cause);
 123         }
 124     }
 125 
 126     // Handle casting like Long to Integer, Integer to int, etc
 127     private Object cast(Class<?> desiredType, Object value) throws Exception {
 128         log("ExportedJavaObject.cast: desired=" + desiredType.getSimpleName()
 129                 + ", value=" + (value == null ? "null" : value.getClass().getSimpleName()));
 130         if (value == null) {
 131             return null;
 132         }
 133         // we need wrapper classes instead primitive
 134         if (desiredType.isPrimitive()) {
 135             Class[] wrapperClasses = new Class[]{
 136                 Byte.class, Short.class, Integer.class, Long.class,
 137                 Float.class, Double.class, Boolean.class, Character.class};
 138             for (Class wrap: wrapperClasses) {
 139                 if (desiredType.equals(wrap.getField("TYPE").get(null))) {
 140                     log("ExportedJavaObject.cast: replace " + desiredType.getName()
 141                             + " with " + wrap.getName());
 142                     desiredType = wrap;
 143                     break;
 144                 }
 145             }
 146         }
 147         if (Number.class.isAssignableFrom(value.getClass())) {
 148             Number n = (Number)value;
 149             if (desiredType.equals(Byte.class)) {
 150                 value = Byte.valueOf(n.byteValue());
 151             } else if (desiredType.equals(Short.class)) {
 152                 value = Short.valueOf(n.shortValue());
 153             } else if (desiredType.equals(Integer.class)) {
 154                 value = Integer.valueOf(n.intValue());
 155             } else if (desiredType.equals(Long.class)) {
 156                 value = Long.valueOf(n.longValue());
 157             } else if (desiredType.equals(BigInteger.class)) {
 158                 // handle BigInteger as Long
 159                 value = BigInteger.valueOf(n.longValue());
 160             } else if (desiredType.equals(Float.class)) {
 161                 value = Float.valueOf(n.floatValue());
 162             } else if (desiredType.equals(Double.class)) {
 163                 value = Double.valueOf(n.doubleValue());
 164             } else if (desiredType.equals(BigDecimal.class)) {
 165                 // handle BigDecimal as Double
 166                 value = BigDecimal.valueOf(n.doubleValue());
 167             }
 168         }
 169         return desiredType.cast(value);
 170     }
 171 
 172     // returns JSON-encoded result
 173     public String call(final String methodName, final String args) throws CallException {
 174         try {
 175             return AccessController.doPrivileged(new PrivilegedExceptionAction<String>() {
 176                 @Override
 177                 public String run() throws Exception {
 178                     return callWorker(methodName, args);
 179                 }
 180             }, getJSBridge().getAccessControlContext());
 181         } catch (Exception e) {
 182             if (e instanceof CallException) {
 183                 throw (CallException) e;
 184             }
 185 
 186             throw new UndeclaredThrowableException(e);
 187         }
 188     }
 189 
 190     private String callWorker(String methodName, String args) throws CallException {
 191         // methodName is an index in methods array
 192         Method m;
 193         try {
 194             int mIndex = Integer.parseInt(methodName);  // NumberFormatException
 195             m = methods[mIndex];    // ArrayIndexOutOfBoundsException
 196         } catch (Exception ex) {
 197             throw new CallException("Wrong method id", ex);
 198         }
 199 
 200         log("call: " + javaObject.getClass().getSimpleName()
 201                 + "." + m.getName() + "(" + args + ")");
 202 
 203         // we always encode paramaters as array
 204         Object[] params;
 205         try {
 206             Object argObj = getJSBridge().decode(args);
 207             params = (Object[])argObj;
 208         } catch (Exception ex) {
 209             throw new CallException("Could not parse arguments", ex);
 210         }
 211 
 212         Class<?>[] paramTypes = m.getParameterTypes();
 213         if (paramTypes.length != params.length) {
 214             throw new CallException("Incorrect argument number: "
 215                     + params.length + " instead " + paramTypes);
 216         }
 217 
 218         Object[] castedParams = new Object[paramTypes.length];
 219         int i = 0;
 220         try {
 221             for (i=0; i<paramTypes.length; i++) {
 222                 castedParams[i] = cast(paramTypes[i], params[i]);
 223             }
 224         } catch (Exception ex) {
 225             throw new CallException("Argument " + i + " casting error", ex);
 226         }
 227 
 228         Object resObj;
 229         try {
 230             resObj = m.invoke(javaObject, castedParams);
 231             // return null if the method is void
 232             if (Void.class.equals(m.getReturnType())) {
 233                 return null;
 234             }
 235         } catch (InvocationTargetException ex) {
 236             // the method throws ex.getTargetException()
 237             Throwable reason = ex.getTargetException();
 238             throw new CallException("Java Exception ("
 239                     + reason.getClass().getSimpleName() + "): "
 240                     + reason.getMessage(), reason);
 241         } catch (Exception ex) {
 242             // IllegalAccessException, IllegalArgumentException,
 243             // NullPointerException, ExceptionInInitializerError
 244             throw new CallException("Invoke error ("
 245                     + ex.getClass().getSimpleName() + "): "
 246                     + ex.getMessage(), ex);
 247         }
 248 
 249         try {
 250             StringBuilder sb = new StringBuilder(1024);
 251             getJSBridge().encode(resObj, sb);
 252             return sb.toString();
 253         } catch (Exception ex) {
 254             throw new CallException("Result encoding error", ex);
 255         }
 256     }
 257 
 258     private Method[] getPublicMethods(final Class clz) {
 259         Method[] m = clz.getMethods();
 260         ArrayList<Method> am = new ArrayList<Method>();
 261         for (int i = 0; i < m.length; i++) {
 262             if (Modifier.isPublic(m[i].getModifiers())){
 263                 am.add(m[i]);
 264             }
 265         }
 266 
 267         Method[] publicMethods = new Method[am.size()];
 268         return am.toArray(publicMethods);
 269     }
 270 
 271     static void log(String s) {
 272         JS2JavaBridge.log(s);
 273     }
 274 
 275     static void log(Exception ex) {
 276         JS2JavaBridge.log(ex);
 277     }
 278 
 279 }