1 // Copyright 2007 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); You may not 4 // use this file except in compliance with the License. You may obtain a copy of 5 // the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by 6 // applicable law or agreed to in writing, software distributed under the 7 // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 8 // OF ANY KIND, either express or implied. See the License for the specific 9 // language governing permissions and limitations under the License. 10 11 package com.google.scrollview; 12 13 import com.google.scrollview.events.SVEvent; 14 import com.google.scrollview.ui.SVImageHandler; 15 import com.google.scrollview.ui.SVWindow; 16 17 import java.io.BufferedReader; 18 import java.io.IOException; 19 import java.io.InputStreamReader; 20 import java.io.PrintStream; 21 import java.net.ServerSocket; 22 import java.net.Socket; 23 import java.util.ArrayList; 24 import java.util.regex.Pattern; 25 26 27 /** 28 * The ScrollView class is the main class which gets started from the command 29 * line. It sets up LUA and handles the network processing. 30 * @author wanke@google.com 31 */ 32 public class ScrollView { 33 34 /** The port our server listens at. */ 35 public static int SERVER_PORT = 8461; 36 37 /** 38 * All SVWindow objects share the same connection stream. The socket is needed 39 * to detect when the connection got closed, in/out are used to send and 40 * receive messages. 41 */ 42 private static Socket socket; 43 private static PrintStream out; 44 public static BufferedReader in; 45 public static float polylineXCoords[]; // The coords being received. 46 public static float polylineYCoords[]; // The coords being received. 47 public static int polylineSize; // The size of the coords arrays. 48 public static int polylineScanned; // The size read so far. 49 private static ArrayList<SVWindow> windows; // The id to SVWindow map. 50 private static Pattern intPattern; // For checking integer arguments. 51 private static Pattern floatPattern; // For checking float arguments. 52 53 /** Keeps track of the number of messages received. */ 54 static int nrInputLines = 0; 55 56 /** Prints all received messages to the console if true. */ 57 static boolean debugViewNetworkTraffic = false; 58 59 /** Add a new message to the outgoing queue */ addMessage(SVEvent e)60 public static void addMessage(SVEvent e) { 61 if (debugViewNetworkTraffic) { 62 System.out.println("(S->c) " + e.toString()); 63 } 64 String str = e.toString(); 65 // Send the whole thing as UTF8. 66 try { 67 byte [] utf8 = str.getBytes("UTF8"); 68 out.write(utf8, 0, utf8.length); 69 } catch (java.io.UnsupportedEncodingException ex) { 70 System.out.println("Oops... can't encode to UTF8... Exiting"); 71 System.exit(0); 72 } 73 out.println(); 74 // Flush the output and check for errors. 75 boolean error = out.checkError(); 76 if (error) { 77 System.out.println("Connection error. Quitting ScrollView Server..."); 78 System.exit(0); 79 } 80 } 81 82 /** Read one message from client (assuming there are any). */ receiveMessage()83 public static String receiveMessage() throws IOException { 84 return in.readLine(); 85 } 86 87 /** 88 * The main program loop. Basically loops trough receiving messages and 89 * processing them and then sending messages (if there are any). 90 */ IOLoop()91 private static void IOLoop() { 92 String inputLine; 93 94 try { 95 while (!socket.isClosed() && !socket.isInputShutdown() && 96 !socket.isOutputShutdown() && 97 socket.isConnected() && socket.isBound()) { 98 inputLine = receiveMessage(); 99 nrInputLines++; 100 if (debugViewNetworkTraffic) { 101 System.out.println("(c->S," + nrInputLines + ")" + inputLine); 102 } 103 104 if (polylineSize > polylineScanned) { 105 // We are processing a polyline. 106 // Read pairs of coordinates separated by commas. 107 boolean first = true; 108 for (String coordStr : inputLine.split(",")) { 109 int coord = Integer.parseInt(coordStr); 110 if (first) { 111 polylineXCoords[polylineScanned] = coord; 112 } else { 113 polylineYCoords[polylineScanned++] = coord; 114 } 115 first = !first; 116 } 117 assert first; 118 } else if (SVImageHandler.getReadImageData() == false) { 119 // If we are currently not transmitting an image, process this 120 // normally. 121 processInput(inputLine); 122 } 123 // We are still transmitting image data, but there seems to be some 124 // command at the 125 // end of the message attached as well. Thus, we have to split it 126 // accordingly and 127 // first generate the image and afterwards process the remaining 128 // message. 129 else if (inputLine.length() > 130 SVImageHandler.getMissingRemainingBytes()) { 131 String luaCmd = inputLine.substring( 132 SVImageHandler.getMissingRemainingBytes()); 133 String imgData = inputLine.substring(0, 134 SVImageHandler.getMissingRemainingBytes()); 135 SVImageHandler.parseData(imgData); 136 processInput(luaCmd); 137 } else { // We are still in the middle of image data and have not 138 // reached the end yet. 139 SVImageHandler.parseData(inputLine); 140 } 141 } 142 } 143 // Some connection error 144 catch (IOException e) { 145 System.out.println("Connection error. Quitting ScrollView Server..."); 146 } 147 System.exit(0); 148 } 149 150 // Parse a comma-separated list of arguments into ArrayLists of the 151 // possible types. Each type is stored in order, but the order 152 // distinction between types is lost. 153 // Note that the format is highly constrained to what the client used 154 // to send to LUA: 155 // Quoted string -> String. 156 // true or false -> Boolean. 157 // %f format number -> Float (no %e allowed) 158 // Sequence of digits -> Integer 159 // Nothing else allowed. parseArguments(String argList, ArrayList<Integer> intList, ArrayList<Float> floatList, ArrayList<String> stringList, ArrayList<Boolean> boolList)160 private static void parseArguments(String argList, 161 ArrayList<Integer> intList, 162 ArrayList<Float> floatList, 163 ArrayList<String> stringList, 164 ArrayList<Boolean> boolList) { 165 // str is only non-null if an argument starts with a single or double 166 // quote. str is set back to null on completion of the string with a 167 // matching quote. If the string contains a comma then str will stay 168 // non-null across multiple argStr values until a matching closing quote. 169 // Backslash escaped quotes do not count as terminating the string. 170 String str = null; 171 for (String argStr : argList.split(",")) { 172 if (str != null) { 173 // Last string was incomplete. Append argStr to it and restore comma. 174 // Execute str += "," + argStr in Java. 175 int length = str.length() + 1 + argStr.length(); 176 StringBuilder appended = new StringBuilder(length); 177 appended.append(str); 178 appended.append(","); 179 appended.append(argStr); 180 str = appended.toString(); 181 } else if (argStr.length() == 0) { 182 continue; 183 } else { 184 char quote = argStr.charAt(0); 185 // If it begins with a quote then it is a string, but may not 186 // end this time if it contained a comma. 187 if (quote == '\'' || quote == '"') { 188 str = argStr; 189 } 190 } 191 if (str != null) { 192 // It began with a quote. Check that it still does. 193 assert str.charAt(0) == '\'' || str.charAt(0) == '"'; 194 int len = str.length(); 195 if (len > 1 && str.charAt(len - 1) == str.charAt(0)) { 196 // We have an ending quote of the right type. Now check that 197 // it is not escaped. Must have an even number of slashes before. 198 int slash = len - 1; 199 while (slash > 0 && str.charAt(slash - 1) == '\\') 200 --slash; 201 if ((len - 1 - slash) % 2 == 0) { 202 // It is now complete. Chop off the quotes and save. 203 // TODO(rays) remove the first backslash of each pair. 204 stringList.add(str.substring(1, len - 1)); 205 str = null; 206 } 207 } 208 // If str is not null here, then we have a string with a comma in it. 209 // Append , and the next argument at the next iteration, but check 210 // that str is null after the loop terminates in case it was an 211 // unterminated string. 212 } else if (floatPattern.matcher(argStr).matches()) { 213 // It is a float. 214 floatList.add(Float.parseFloat(argStr)); 215 } else if (argStr.equals("true")) { 216 boolList.add(true); 217 } else if (argStr.equals("false")) { 218 boolList.add(false); 219 } else if (intPattern.matcher(argStr).matches()) { 220 // Only contains digits so must be an int. 221 intList.add(Integer.parseInt(argStr)); 222 } 223 // else ignore all incompatible arguments for forward compatibility. 224 } 225 // All strings must have been terminated. 226 assert str == null; 227 } 228 229 /** Executes the LUA command parsed as parameter. */ processInput(String inputLine)230 private static void processInput(String inputLine) { 231 // Execute a function encoded as a LUA statement! Yuk! 232 if (inputLine.charAt(0) == 'w') { 233 // This is a method call on a window. Parse it. 234 String noWLine = inputLine.substring(1); 235 String[] idStrs = noWLine.split("[ :]", 2); 236 int windowID = Integer.parseInt(idStrs[0]); 237 // Find the parentheses. 238 int start = inputLine.indexOf('('); 239 int end = inputLine.lastIndexOf(')'); 240 // Parse the args. 241 ArrayList<Integer> intList = new ArrayList<Integer>(4); 242 ArrayList<Float> floatList = new ArrayList<Float>(2); 243 ArrayList<String> stringList = new ArrayList<String>(4); 244 ArrayList<Boolean> boolList = new ArrayList<Boolean>(3); 245 parseArguments(inputLine.substring(start + 1, end), 246 intList, floatList, stringList, boolList); 247 int colon = inputLine.indexOf(':'); 248 if (colon > 1 && colon < start) { 249 // This is a regular function call. Look for the name and call it. 250 String func = inputLine.substring(colon + 1, start); 251 if (func.equals("drawLine")) { 252 windows.get(windowID).drawLine(intList.get(0), intList.get(1), 253 intList.get(2), intList.get(3)); 254 } else if (func.equals("createPolyline")) { 255 windows.get(windowID).createPolyline(intList.get(0)); 256 } else if (func.equals("drawPolyline")) { 257 windows.get(windowID).drawPolyline(); 258 } else if (func.equals("drawRectangle")) { 259 windows.get(windowID).drawRectangle(intList.get(0), intList.get(1), 260 intList.get(2), intList.get(3)); 261 } else if (func.equals("setVisible")) { 262 windows.get(windowID).setVisible(boolList.get(0)); 263 } else if (func.equals("setAlwaysOnTop")) { 264 windows.get(windowID).setAlwaysOnTop(boolList.get(0)); 265 } else if (func.equals("addMessage")) { 266 windows.get(windowID).addMessage(stringList.get(0)); 267 } else if (func.equals("addMessageBox")) { 268 windows.get(windowID).addMessageBox(); 269 } else if (func.equals("clear")) { 270 windows.get(windowID).clear(); 271 } else if (func.equals("setStrokeWidth")) { 272 windows.get(windowID).setStrokeWidth(floatList.get(0)); 273 } else if (func.equals("drawEllipse")) { 274 windows.get(windowID).drawEllipse(intList.get(0), intList.get(1), 275 intList.get(2), intList.get(3)); 276 } else if (func.equals("pen")) { 277 if (intList.size() == 4) { 278 windows.get(windowID).pen(intList.get(0), intList.get(1), 279 intList.get(2), intList.get(3)); 280 } else { 281 windows.get(windowID).pen(intList.get(0), intList.get(1), 282 intList.get(2)); 283 } 284 } else if (func.equals("brush")) { 285 if (intList.size() == 4) { 286 windows.get(windowID).brush(intList.get(0), intList.get(1), 287 intList.get(2), intList.get(3)); 288 } else { 289 windows.get(windowID).brush(intList.get(0), intList.get(1), 290 intList.get(2)); 291 } 292 } else if (func.equals("textAttributes")) { 293 windows.get(windowID).textAttributes(stringList.get(0), 294 intList.get(0), 295 boolList.get(0), 296 boolList.get(1), 297 boolList.get(2)); 298 } else if (func.equals("drawText")) { 299 windows.get(windowID).drawText(intList.get(0), intList.get(1), 300 stringList.get(0)); 301 } else if (func.equals("openImage")) { 302 windows.get(windowID).openImage(stringList.get(0)); 303 } else if (func.equals("drawImage")) { 304 windows.get(windowID).drawImage(stringList.get(0), 305 intList.get(0), intList.get(1)); 306 } else if (func.equals("addMenuBarItem")) { 307 if (boolList.size() > 0) { 308 windows.get(windowID).addMenuBarItem(stringList.get(0), 309 stringList.get(1), 310 intList.get(0), 311 boolList.get(0)); 312 } else if (intList.size() > 0) { 313 windows.get(windowID).addMenuBarItem(stringList.get(0), 314 stringList.get(1), 315 intList.get(0)); 316 } else { 317 windows.get(windowID).addMenuBarItem(stringList.get(0), 318 stringList.get(1)); 319 } 320 } else if (func.equals("addPopupMenuItem")) { 321 if (stringList.size() == 4) { 322 windows.get(windowID).addPopupMenuItem(stringList.get(0), 323 stringList.get(1), 324 intList.get(0), 325 stringList.get(2), 326 stringList.get(3)); 327 } else { 328 windows.get(windowID).addPopupMenuItem(stringList.get(0), 329 stringList.get(1)); 330 } 331 } else if (func.equals("update")) { 332 windows.get(windowID).update(); 333 } else if (func.equals("showInputDialog")) { 334 windows.get(windowID).showInputDialog(stringList.get(0)); 335 } else if (func.equals("showYesNoDialog")) { 336 windows.get(windowID).showYesNoDialog(stringList.get(0)); 337 } else if (func.equals("zoomRectangle")) { 338 windows.get(windowID).zoomRectangle(intList.get(0), intList.get(1), 339 intList.get(2), intList.get(3)); 340 } else if (func.equals("createImage")) { 341 windows.get(windowID).createImage(stringList.get(0), intList.get(0), 342 intList.get(1), intList.get(2)); 343 } else if (func.equals("drawImage")) { 344 windows.get(windowID).drawImage(stringList.get(0), 345 intList.get(0), intList.get(1)); 346 } else if (func.equals("destroy")) { 347 windows.get(windowID).destroy(); 348 } 349 // else for forward compatibility purposes, silently ignore any 350 // unrecognized function call. 351 } else { 352 // No colon. Check for create window. 353 if (idStrs[1].startsWith("= luajava.newInstance")) { 354 while (windows.size() <= windowID) { 355 windows.add(null); 356 } 357 windows.set(windowID, new SVWindow(stringList.get(1), 358 intList.get(0), intList.get(1), 359 intList.get(2), intList.get(3), 360 intList.get(4), intList.get(5), 361 intList.get(6))); 362 } 363 // else for forward compatibility purposes, silently ignore any 364 // unrecognized function call. 365 } 366 } else if (inputLine.startsWith("svmain")) { 367 // Startup or end. Startup is a lua bind, which is now a no-op. 368 if (inputLine.startsWith("svmain:exit")) { 369 exit(); 370 } 371 // else for forward compatibility purposes, silently ignore any 372 // unrecognized function call. 373 } 374 // else for forward compatibility purposes, silently ignore any 375 // unrecognized function call. 376 } 377 378 /** Called from the client to make the server exit. */ exit()379 public static void exit() { 380 System.exit(0); 381 } 382 383 /** 384 * The main function. Sets up LUA and the server connection and then calls the 385 * IOLoop. 386 */ main(String[] args)387 public static void main(String[] args) { 388 if (args.length > 0) { 389 SERVER_PORT = Integer.parseInt(args[0]); 390 } 391 windows = new ArrayList<SVWindow>(100); 392 intPattern = Pattern.compile("[0-9-][0-9]*"); 393 floatPattern = Pattern.compile("[0-9-][0-9]*\\.[0-9]*"); 394 395 try { 396 // Open a socket to listen on. 397 ServerSocket serverSocket = new ServerSocket(SERVER_PORT); 398 System.out.println("Socket started on port " + SERVER_PORT); 399 400 // Wait (blocking) for an incoming connection 401 socket = serverSocket.accept(); 402 System.out.println("Client connected"); 403 404 // Setup the streams 405 out = new PrintStream(socket.getOutputStream(), true); 406 in = 407 new BufferedReader(new InputStreamReader(socket.getInputStream(), 408 "UTF8")); 409 } catch (IOException e) { 410 // Something went wrong and we were unable to set up a connection. This is 411 // pretty 412 // much a fatal error. 413 // Note: The server does not get restarted automatically if this happens. 414 e.printStackTrace(); 415 System.exit(1); 416 } 417 418 // Enter the main program loop. 419 IOLoop(); 420 } 421 } 422