diff options
Diffstat (limited to 'src/com/vaadin/terminal/gwt/server/CommunicationManager.java')
-rw-r--r-- | src/com/vaadin/terminal/gwt/server/CommunicationManager.java | 1497 |
1 files changed, 1497 insertions, 0 deletions
diff --git a/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java new file mode 100644 index 0000000000..c43c857c1a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java @@ -0,0 +1,1497 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemIterator; +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemStream; +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileUploadException; +import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload; +import com.vaadin.Application; +import com.vaadin.Application.SystemMessages; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.URIHandler; +import com.vaadin.terminal.UploadStream; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.Paintable.RepaintRequestEvent; +import com.vaadin.terminal.Terminal.ErrorEvent; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Component; +import com.vaadin.ui.Upload; +import com.vaadin.ui.Window; + +/** + * Application manager processes changes and paints for single application + * instance. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class CommunicationManager implements Paintable.RepaintRequestListener, + Serializable { + + private static String GET_PARAM_REPAINT_ALL = "repaintAll"; + + /* Variable records indexes */ + private static final int VAR_PID = 1; + private static final int VAR_NAME = 2; + private static final int VAR_TYPE = 3; + private static final int VAR_VALUE = 0; + + private static final String VAR_RECORD_SEPARATOR = "\u001e"; + + private static final String VAR_FIELD_SEPARATOR = "\u001f"; + + public static final String VAR_BURST_SEPARATOR = "\u001d"; + + public static final String VAR_ARRAYITEM_SEPARATOR = "\u001c"; + + private final HashSet<String> currentlyOpenWindowsInClient = new HashSet<String>(); + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; + + private final ArrayList<Paintable> dirtyPaintabletSet = new ArrayList<Paintable>(); + + private final HashMap<Paintable, String> paintableIdMap = new HashMap<Paintable, String>(); + + private final HashMap<String, Paintable> idPaintableMap = new HashMap<String, Paintable>(); + + private int idSequence = 0; + + private final AbstractApplicationServlet applicationServlet; + + private final Application application; + + // Note that this is only accessed from synchronized block and + // thus should be thread-safe. + private String closingWindowName = null; + + private List<String> locales; + + private int pendingLocalesIndex; + + private int timeoutInterval = -1; + + public CommunicationManager(Application application, + AbstractApplicationServlet applicationServlet) { + this.application = application; + requireLocale(application.getLocale().toString()); + this.applicationServlet = applicationServlet; + } + + /** + * Handles file upload request submitted via Upload component. + * + * @param request + * @param response + * @throws IOException + * @throws FileUploadException + */ + public void handleFileUpload(HttpServletRequest request, + HttpServletResponse response) throws IOException, + FileUploadException { + // Create a new file upload handler + final ServletFileUpload upload = new ServletFileUpload(); + + final UploadProgressListener pl = new UploadProgressListener(); + + upload.setProgressListener(pl); + + // Parse the request + FileItemIterator iter; + + try { + iter = upload.getItemIterator(request); + /* + * ATM this loop is run only once as we are uploading one file per + * request. + */ + while (iter.hasNext()) { + final FileItemStream item = iter.next(); + final String name = item.getFieldName(); + final String filename = item.getName(); + final String mimeType = item.getContentType(); + final InputStream stream = item.openStream(); + if (item.isFormField()) { + // ignored, upload requests contains only files + } else { + final String pid = name.split("_")[0]; + final Upload uploadComponent = (Upload) idPaintableMap + .get(pid); + if (uploadComponent.isReadOnly()) { + throw new FileUploadException( + "Warning: ignored file upload because upload component is set as read-only"); + } + if (uploadComponent == null) { + throw new FileUploadException( + "Upload component not found"); + } + synchronized (application) { + // put upload component into receiving state + uploadComponent.startUpload(); + } + final UploadStream upstream = new UploadStream() { + + public String getContentName() { + return filename; + } + + public String getContentType() { + return mimeType; + } + + public InputStream getStream() { + return stream; + } + + public String getStreamName() { + return "stream"; + } + + }; + + // tell UploadProgressListener which component is receiving + // file + pl.setUpload(uploadComponent); + + uploadComponent.receiveUpload(upstream); + } + } + } catch (final FileUploadException e) { + throw e; + } + + // Send short response to acknowledge client that request was done + response.setContentType("text/html"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>download handled</body></html>"); + outWriter.flush(); + out.close(); + } + + /** + * Handles UIDL request + * + * @param request + * @param response + * @throws IOException + * @throws ServletException + */ + public void handleUidlRequest(HttpServletRequest request, + HttpServletResponse response, + AbstractApplicationServlet applicationServlet) throws IOException, + ServletException, InvalidUIDLSecurityKeyException { + + // repaint requested or session has timed out and new one is created + boolean repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null) + || request.getSession().isNew(); + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null); + } + + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + // The rest of the process is synchronized with the application + // in order to guarantee that no parallel variable handling is + // made + synchronized (application) { + + // Finds the window within the application + Window window = null; + if (application.isRunning()) { + window = getApplicationWindow(request, application, null); + // Returns if no window found + if (window == null) { + // This should not happen, no windows exists but + // application is still open. + System.err + .println("Warning, could not get window for application with request URI " + + request.getRequestURI()); + return; + } + } else { + // application has been closed + endApplication(request, response, application); + return; + } + + // Change all variables based on request parameters + if (!handleVariables(request, response, application, window)) { + + // var inconsistency; the client is probably out-of-sync + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (Exception e2) { + // FIXME: Handle exception + // Not critical, but something is still wrong; print + // stacktrace + e2.printStackTrace(); + } + if (ci != null) { + String msg = ci.getOutOfSyncMessage(); + String cap = ci.getOutOfSyncCaption(); + if (msg != null || cap != null) { + applicationServlet.criticalNotification(request, + response, cap, msg, ci.getOutOfSyncURL()); + // will reload page after this + return; + } + } + // No message to show, let's just repaint all. + repaintAll = true; + + } + + paintAfterVariablechanges(request, response, applicationServlet, + repaintAll, outWriter, window, analyzeLayouts); + + // Mark this window to be open on client + currentlyOpenWindowsInClient.add(window.getName()); + if (closingWindowName != null) { + currentlyOpenWindowsInClient.remove(closingWindowName); + closingWindowName = null; + } + } + + out.flush(); + out.close(); + } + + private void paintAfterVariablechanges(HttpServletRequest request, + HttpServletResponse response, + AbstractApplicationServlet applicationServlet, boolean repaintAll, + final PrintWriter outWriter, Window window, boolean analyzeLayouts) + throws IOException, ServletException, PaintException { + + // If repaint is requested, clean all ids in this root window + if (repaintAll) { + for (final Iterator it = idPaintableMap.keySet().iterator(); it + .hasNext();) { + final Component c = (Component) idPaintableMap.get(it.next()); + if (isChildOf(window, c)) { + it.remove(); + paintableIdMap.remove(c); + } + } + } + + // Removes application if it has stopped during variable changes + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + // Sets the response type + response.setContentType("application/json; charset=UTF-8"); + // some dirt to prevent cross site scripting + outWriter.print("for(;;);[{"); + + outWriter.print("\"changes\":["); + + ArrayList<Paintable> paintables = null; + + // If the browser-window has been closed - we do not need to paint it at + // all + if (!window.getName().equals(closingWindowName)) { + + List<InvalidLayout> invalidComponentRelativeSizes = null; + + // re-get window - may have been changed + Window newWindow = getApplicationWindow(request, application, + window); + if (newWindow != window) { + window = newWindow; + repaintAll = true; + } + + JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, + !repaintAll); + + // Paints components + if (repaintAll) { + paintables = new ArrayList<Paintable>(); + paintables.add(window); + + // Reset sent locales + locales = null; + requireLocale(application.getLocale().toString()); + + } else { + // remove detached components from paintableIdMap so they + // can be GC'ed + for (Iterator it = paintableIdMap.keySet().iterator(); it + .hasNext();) { + Component p = (Component) it.next(); + if (p.getApplication() == null) { + idPaintableMap.remove(paintableIdMap.get(p)); + it.remove(); + dirtyPaintabletSet.remove(p); + p.removeListener(this); + } + } + paintables = getDirtyVisibleComponents(window); + } + if (paintables != null) { + + // We need to avoid painting children before parent. + // This is ensured by ordering list by depth in component + // tree + Collections.sort(paintables, new Comparator<Paintable>() { + public int compare(Paintable o1, Paintable o2) { + Component c1 = (Component) o1; + Component c2 = (Component) o2; + int d1 = 0; + while (c1.getParent() != null) { + d1++; + c1 = c1.getParent(); + } + int d2 = 0; + while (c2.getParent() != null) { + d2++; + c2 = c2.getParent(); + } + if (d1 < d2) { + return -1; + } + if (d1 > d2) { + return 1; + } + return 0; + } + }); + + for (final Iterator i = paintables.iterator(); i.hasNext();) { + final Paintable p = (Paintable) i.next(); + + // TODO CLEAN + if (p instanceof Window) { + final Window w = (Window) p; + if (w.getTerminal() == null) { + w.setTerminal(application.getMainWindow() + .getTerminal()); + } + } + /* + * This does not seem to happen in tk5, but remember this + * case: else if (p instanceof Component) { if (((Component) + * p).getParent() == null || ((Component) + * p).getApplication() == null) { // Component requested + * repaint, but is no // longer attached: skip + * paintablePainted(p); continue; } } + */ + + // TODO we may still get changes that have been + // rendered already (changes with only cached flag) + if (paintTarget.needsToBePainted(p)) { + paintTarget.startTag("change"); + paintTarget.addAttribute("format", "uidl"); + final String pid = getPaintableId(p); + paintTarget.addAttribute("pid", pid); + + p.paint(paintTarget); + + paintTarget.endTag("change"); + } + paintablePainted(p); + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(((Window) p) + .getLayout(), null, null); + } + } + } + + paintTarget.close(); + outWriter.print("]"); // close changes + + outWriter.print(", \"meta\" : {"); + boolean metaOpen = false; + + if (repaintAll) { + metaOpen = true; + outWriter.write("\"repaintAll\":true"); + if (analyzeLayouts) { + outWriter.write(", \"invalidLayouts\":"); + outWriter.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + outWriter.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(outWriter, this, + System.err); + } + } + outWriter.write("]"); + } + } + + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (NoSuchMethodException e1) { + e1.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (ci != null && ci.getSessionExpiredMessage() == null + && ci.getSessionExpiredCaption() == null + && ci.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = request.getSession() + .getMaxInactiveInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = ci.getSessionExpiredURL() == null ? "" + : ci.getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + outWriter.write(","); + } + outWriter.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + + outWriter.print("}, \"resources\" : {"); + + // Precache custom layouts + String themeName = window.getTheme(); + if (request.getParameter("theme") != null) { + themeName = request.getParameter("theme"); + } + if (themeName == null) { + themeName = "default"; + } + + // TODO We should only precache the layouts that are not + // cached already + int resourceIndex = 0; + for (final Iterator i = paintTarget.getPreCachedResources() + .iterator(); i.hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = applicationServlet + .getServletContext() + .getResourceAsStream( + "/" + + ApplicationServlet.THEME_DIRECTORY_PATH + + themeName + "/" + resource); + } catch (final Exception e) { + // FIXME: Handle exception + e.printStackTrace(); + } + if (is != null) { + + outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" + + resource + "\" : "); + final StringBuffer layout = new StringBuffer(); + + try { + final InputStreamReader r = new InputStreamReader(is, + "UTF-8"); + final char[] buffer = new char[20000]; + int charsRead = 0; + while ((charsRead = r.read(buffer)) > 0) { + layout.append(buffer, 0, charsRead); + } + r.close(); + } catch (final java.io.IOException e) { + // FIXME: Handle exception + System.err.println("Resource transfer failed: " + + request.getRequestURI() + ". (" + + e.getMessage() + ")"); + } + outWriter.print("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + + "\""); + } else { + // FIXME: Handle exception + System.err.println("CustomLayout " + "/" + + ApplicationServlet.THEME_DIRECTORY_PATH + + themeName + "/" + resource + " not found!"); + } + } + outWriter.print("}"); + + printLocaleDeclarations(outWriter); + + outWriter.print("}]"); + } + outWriter.flush(); + outWriter.close(); + + } + + /** + * If this method returns false, something was submitted that we did not + * expect; this is probably due to the client being out-of-sync and sending + * variable changes for non-existing pids + * + * @param request + * @param application2 + * @return true if successful, false if there was an inconsistency + * @throws IOException + */ + private boolean handleVariables(HttpServletRequest request, + HttpServletResponse response, Application application2, + Window window) throws IOException, InvalidUIDLSecurityKeyException { + boolean success = true; + + if (request.getContentLength() > 0) { + String changes = readRequest(request); + + // Manage bursts one by one + final String[] bursts = changes.split(VAR_BURST_SEPARATOR); + + // Security: double cookie submission pattern unless disabled by + // property + if (!"true".equals(application2 + .getProperty("disable-xsrf-protection"))) { + if (bursts.length == 1 && "init".equals(bursts[0])) { + // initial request, no variable changes: send key + String seckey = (String) request.getSession().getAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER); + if (seckey == null) { + seckey = "" + (int) (Math.random() * 1000000); + } + /* + * Cookie c = new Cookie( + * ApplicationConnection.UIDL_SECURITY_COOKIE_NAME, uuid); + * response.addCookie(c); + */ + response.setHeader( + ApplicationConnection.UIDL_SECURITY_HEADER, seckey); + request.getSession().setAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER, seckey); + return true; + } else { + // check the key + String sessId = (String) request.getSession().getAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER); + if (sessId == null || !sessId.equals(bursts[0])) { + throw new InvalidUIDLSecurityKeyException( + "Security key mismatch"); + } + } + } + + for (int bi = 1; bi < bursts.length; bi++) { + + // extract variables to two dim string array + final String[] tmp = bursts[bi].split(VAR_RECORD_SEPARATOR); + final String[][] variableRecords = new String[tmp.length][4]; + for (int i = 0; i < tmp.length; i++) { + variableRecords[i] = tmp[i].split(VAR_FIELD_SEPARATOR); + } + + for (int i = 0; i < variableRecords.length; i++) { + String[] variable = variableRecords[i]; + String[] nextVariable = null; + if (i + 1 < variableRecords.length) { + nextVariable = variableRecords[i + 1]; + } + final VariableOwner owner = (VariableOwner) idPaintableMap + .get(variable[VAR_PID]); + if (owner != null && owner.isEnabled()) { + Map m; + if (nextVariable != null + && variable[VAR_PID] + .equals(nextVariable[VAR_PID])) { + // we have more than one value changes in row for + // one variable owner, collect em in HashMap + m = new HashMap(); + m.put(variable[VAR_NAME], convertVariableValue( + variable[VAR_TYPE].charAt(0), + variable[VAR_VALUE])); + } else { + // use optimized single value map + m = new SingleValueMap(variable[VAR_NAME], + convertVariableValue(variable[VAR_TYPE] + .charAt(0), variable[VAR_VALUE])); + } + + // collect following variable changes for this owner + while (nextVariable != null + && variable[VAR_PID] + .equals(nextVariable[VAR_PID])) { + i++; + variable = nextVariable; + if (i + 1 < variableRecords.length) { + nextVariable = variableRecords[i + 1]; + } else { + nextVariable = null; + } + m.put(variable[VAR_NAME], convertVariableValue( + variable[VAR_TYPE].charAt(0), + variable[VAR_VALUE])); + } + try { + owner.changeVariables(request, m); + + // Special-case of closing browser-level windows: + // track browser-windows currently open in client + if (owner instanceof Window + && ((Window) owner).getParent() == null) { + final Boolean close = (Boolean) m.get("close"); + if (close != null && close.booleanValue()) { + closingWindowName = ((Window) owner) + .getName(); + } + } + } catch (Exception e) { + handleChangeVariablesError(application2, + (Component) owner, e, m); + } + } else { + + // Handle special case where window-close is called + // after the window has been removed from the + // application or the application has closed + if ("close".equals(variable[VAR_NAME]) + && "true".equals(variable[VAR_VALUE])) { + // Silently ignore this + continue; + } + + // Ignore variable change + String msg = "Warning: Ignoring variable change for "; + if (owner != null) { + msg += "disabled component " + owner.getClass(); + String caption = ((Component) owner).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } else { + msg += "non-existent component, VAR_PID=" + + variable[VAR_PID]; + success = false; + } + System.err.println(msg); + continue; + } + } + + // In case that there were multiple bursts, we know that this is + // a special synchronous case for closing window. Thus we are + // not interested in sending any UIDL changes back to client. + // Still we must clear component tree between bursts to ensure + // that no removed components are updated. The painting after + // the last burst is handled normally by the calling method. + if (bi < bursts.length - 1) { + + // We will be discarding all changes + final PrintWriter outWriter = new PrintWriter( + new CharArrayWriter()); + try { + paintAfterVariablechanges(request, response, + applicationServlet, true, outWriter, window, + false); + } catch (ServletException e) { + // We will ignore all servlet exceptions + } + } + + } + } + return success; + } + + /** + * Reads the request data from the HttpServletRequest and returns it + * converted to an UTF-8 string. + * + * @param request + * @return + * @throws IOException + */ + private static String readRequest(HttpServletRequest request) + throws IOException { + + int requestLength = request.getContentLength(); + + byte[] buffer = new byte[requestLength]; + ServletInputStream inputStream = request.getInputStream(); + + int bytesRemaining = requestLength; + while (bytesRemaining > 0) { + int bytesToRead = Math.min(bytesRemaining, MAX_BUFFER_SIZE); + int bytesRead = inputStream.read(buffer, requestLength + - bytesRemaining, bytesToRead); + if (bytesRead == -1) { + break; + } + + bytesRemaining -= bytesRead; + } + + String result = new String(buffer, "utf-8"); + + return result; + } + + public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable { + private final Throwable throwable; + + public ErrorHandlerErrorEvent(Throwable throwable) { + this.throwable = throwable; + } + + public Throwable getThrowable() { + return throwable; + } + + } + + private void handleChangeVariablesError(Application application, + Component owner, Exception e, Map m) { + boolean handled = false; + ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent( + owner, e, m); + + if (owner instanceof AbstractField) { + try { + handled = ((AbstractField) owner).handleError(errorEvent); + } catch (Exception handlerException) { + /* + * If there is an error in the component error handler we pass + * the that error to the application error handler and continue + * processing the actual error + */ + application.getErrorHandler().terminalError( + new ErrorHandlerErrorEvent(handlerException)); + handled = false; + } + } + + if (!handled) { + application.getErrorHandler().terminalError(errorEvent); + } + + } + + private Object convertVariableValue(char variableType, String strValue) { + Object val = null; + switch (variableType) { + case 'a': + val = strValue.split(VAR_ARRAYITEM_SEPARATOR); + break; + case 's': + val = strValue; + break; + case 'i': + val = Integer.valueOf(strValue); + break; + case 'l': + val = Long.valueOf(strValue); + break; + case 'f': + val = Float.valueOf(strValue); + break; + case 'd': + val = Double.valueOf(strValue); + break; + case 'b': + val = Boolean.valueOf(strValue); + break; + case 'p': + val = idPaintableMap.get(strValue); + break; + } + + return val; + } + + private void printLocaleDeclarations(PrintWriter outWriter) { + /* + * ----------------------------- Sending Locale sensitive date + * ----------------------------- + */ + + // Send locale informations to client + outWriter.print(", \"locales\":["); + for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + outWriter.print("{\"name\":\"" + l.toString() + "\","); + + /* + * Month names (both short and full) + */ + final DateFormatSymbols dfs = new DateFormatSymbols(l); + final String[] short_months = dfs.getShortMonths(); + final String[] months = dfs.getMonths(); + outWriter.print("\"smn\":[\"" + + // ShortMonthNames + short_months[0] + "\",\"" + short_months[1] + "\",\"" + + short_months[2] + "\",\"" + short_months[3] + "\",\"" + + short_months[4] + "\",\"" + short_months[5] + "\",\"" + + short_months[6] + "\",\"" + short_months[7] + "\",\"" + + short_months[8] + "\",\"" + short_months[9] + "\",\"" + + short_months[10] + "\",\"" + short_months[11] + "\"" + + "],"); + outWriter.print("\"mn\":[\"" + + // MonthNames + months[0] + "\",\"" + months[1] + "\",\"" + months[2] + + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" + + months[5] + "\",\"" + months[6] + "\",\"" + months[7] + + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" + + months[10] + "\",\"" + months[11] + "\"" + "],"); + + /* + * Weekday names (both short and full) + */ + final String[] short_days = dfs.getShortWeekdays(); + final String[] days = dfs.getWeekdays(); + outWriter.print("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + outWriter.print("\"dn\":[\"" + + // DayNames + days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" + + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" + + days[7] + "\"" + "],"); + + /* + * First day of week (0 = sunday, 1 = monday) + */ + final Calendar cal = new GregorianCalendar(l); + outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); + + /* + * Date formatting (MM/DD/YYYY etc.) + */ + + DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, l); + if (!(dateFormat instanceof SimpleDateFormat)) { + System.err + .println("Unable to get default date pattern for locale " + + l.toString()); + dateFormat = new SimpleDateFormat(); + } + final String df = ((SimpleDateFormat) dateFormat).toPattern(); + + int timeStart = df.indexOf("H"); + if (timeStart < 0) { + timeStart = df.indexOf("h"); + } + final int ampm_first = df.indexOf("a"); + // E.g. in Korean locale AM/PM is before h:mm + // TODO should take that into consideration on client-side as well, + // now always h:mm a + if (ampm_first > 0 && ampm_first < timeStart) { + timeStart = ampm_first; + } + final String dateformat = df.substring(0, timeStart - 1); + + outWriter.print("\"df\":\"" + dateformat.trim() + "\","); + + /* + * Time formatting (24 or 12 hour clock and AM/PM suffixes) + */ + final String timeformat = df.substring(timeStart, df.length()); + /* + * Doesn't return second or milliseconds. + * + * We use timeformat to determine 12/24-hour clock + */ + final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; + // TODO there are other possibilities as well, like 'h' in french + // (ignore them, too complicated) + final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." + : ":"; + // outWriter.print("\"tf\":\"" + timeformat + "\","); + outWriter.print("\"thc\":" + twelve_hour_clock + ","); + outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + outWriter.print("}"); + if (pendingLocalesIndex < locales.size() - 1) { + outWriter.print(","); + } + } + outWriter.print("]"); // Close locales + } + + /** + * Gets the existing application or create a new one. Get a window within an + * application based on the requested URI. + * + * @param request + * the HTTP Request. + * @param application + * the Application to query for window. + * @param assumedWindow + * if the window has been already resolved once, this parameter + * must contain the window. + * @return Window mathing the given URI or null if not found. + * @throws ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + private Window getApplicationWindow(HttpServletRequest request, + Application application, Window assumedWindow) + throws ServletException { + + Window window = null; + + // If the client knows which window to use, use it if possible + String windowClientRequestedName = request.getParameter("windowName"); + if (assumedWindow != null + && application.getWindows().contains(assumedWindow)) { + windowClientRequestedName = assumedWindow.getName(); + } + if (windowClientRequestedName != null) { + window = application.getWindow(windowClientRequestedName); + if (window != null) { + return window; + } + } + + // If client does not know what window it wants + if (window == null) { + + // Get the path from URL + String path = applicationServlet.getRequestPathInfo(request); + path = path.substring("/UIDL".length()); + + // If the path is specified, create name from it + if (path != null && path.length() > 0 && !path.equals("/")) { + String windowUrlName = null; + if (path.charAt(0) == '/') { + path = path.substring(1); + } + final int index = path.indexOf('/'); + if (index < 0) { + windowUrlName = path; + path = ""; + } else { + windowUrlName = path.substring(0, index); + path = path.substring(index + 1); + } + + window = application.getWindow(windowUrlName); + } + } + + // By default, use mainwindow + if (window == null) { + window = application.getMainWindow(); + } + + // If the requested window is already open, resolve conflict + if (currentlyOpenWindowsInClient.contains(window.getName())) { + String newWindowName = window.getName(); + while (currentlyOpenWindowsInClient.contains(newWindowName)) { + newWindowName = window.getName() + "_" + + ((int) (Math.random() * 100000000)); + } + + window = application.getWindow(newWindowName); + + // If everything else fails, use main window even in case of + // conflicts + if (window == null) { + window = application.getMainWindow(); + } + } + + return window; + } + + /** + * Ends the Application. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @param application + * the Application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(HttpServletRequest request, + HttpServletResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + // clients JS app is still running, send a special json file to tell + // client that application has quit and where to point browser now + // Set the response type + response.setContentType("application/json; charset=UTF-8"); + final ServletOutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("for(;;);[{"); + outWriter.print("\"redirect\":{"); + outWriter.write("\"url\":\"" + logoutUrl + "\"}}]"); + outWriter.flush(); + outWriter.close(); + out.flush(); + } + + /** + * Gets the Paintable Id. If Paintable has debug id set it will be used + * prefixed with "PID_S". Otherwise a sequenced ID is created. + * + * @param paintable + * @return the paintable Id. + */ + public String getPaintableId(Paintable paintable) { + + String id = paintableIdMap.get(paintable); + if (id == null) { + // use testing identifier as id if set + id = paintable.getDebugId(); + if (id == null) { + id = "PID" + Integer.toString(idSequence++); + } else { + id = "PID_S" + id; + } + Paintable old = idPaintableMap.put(id, paintable); + if (old != null && old != paintable) { + /* + * Two paintables have the same id. We still make sure the old + * one is a component which is still attached to the + * application. This is just a precaution and should not be + * absolutely necessary. + */ + + if (old instanceof Component + && ((Component) old).getApplication() != null) { + throw new IllegalStateException("Two paintables (" + + paintable.getClass().getSimpleName() + "," + + old.getClass().getSimpleName() + + ") have been assigned the same id: " + + paintable.getDebugId()); + } + } + paintableIdMap.put(paintable, id); + } + + return id; + } + + public boolean hasPaintableId(Paintable paintable) { + return paintableIdMap.containsKey(paintable); + } + + /** + * Returns dirty components which are in given window. Components in an + * invisible subtrees are omitted. + * + * @param w + * root window for which dirty components is to be fetched + * @return + */ + private ArrayList<Paintable> getDirtyVisibleComponents(Window w) { + final ArrayList<Paintable> resultset = new ArrayList<Paintable>( + dirtyPaintabletSet); + + // The following algorithm removes any components that would be painted + // as a direct descendant of other components from the dirty components + // list. The result is that each component should be painted exactly + // once and any unmodified components will be painted as "cached=true". + + for (final Iterator i = dirtyPaintabletSet.iterator(); i.hasNext();) { + final Paintable p = (Paintable) i.next(); + if (p instanceof Component) { + final Component component = (Component) p; + if (component.getApplication() == null) { + // component is detached after requestRepaint is called + resultset.remove(p); + i.remove(); + } else { + Window componentsRoot = component.getWindow(); + if (componentsRoot.getParent() != null) { + // this is a subwindow + componentsRoot = (Window) componentsRoot.getParent(); + } + if (componentsRoot != w) { + resultset.remove(p); + } else if (component.getParent() != null + && !component.getParent().isVisible()) { + /* + * Do not return components in an invisible subtree. + * + * Components that are invisible in visible subree, must + * be rendered (to let client know that they need to be + * hidden). + */ + resultset.remove(p); + } + } + } + } + + return resultset; + } + + /** + * @see com.vaadin.terminal.Paintable.RepaintRequestListener#repaintRequested(com.vaadin.terminal.Paintable.RepaintRequestEvent) + */ + public void repaintRequested(RepaintRequestEvent event) { + final Paintable p = event.getPaintable(); + if (!dirtyPaintabletSet.contains(p)) { + dirtyPaintabletSet.add(p); + } + } + + /** + * + * @param p + */ + private void paintablePainted(Paintable p) { + dirtyPaintabletSet.remove(p); + p.requestRepaintRequests(); + } + + private final class SingleValueMap implements Map<Object, Object>, + Serializable { + + private final String name; + + private final Object value; + + private SingleValueMap(String name, Object value) { + this.name = name; + this.value = value; + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public boolean containsKey(Object key) { + if (name == null) { + return key == null; + } + return name.equals(key); + } + + public boolean containsValue(Object v) { + if (value == null) { + return v == null; + } + return value.equals(v); + } + + public Set entrySet() { + final Set s = new HashSet(); + s.add(new Map.Entry() { + + public Object getKey() { + return name; + } + + public Object getValue() { + return value; + } + + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + }); + return s; + } + + public Object get(Object key) { + if (!name.equals(key)) { + return null; + } + return value; + } + + public boolean isEmpty() { + return false; + } + + public Set keySet() { + final Set s = new HashSet(); + s.add(name); + return s; + } + + public Object put(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map t) { + throw new UnsupportedOperationException(); + } + + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + public int size() { + return 1; + } + + public Collection values() { + final LinkedList s = new LinkedList(); + s.add(value); + return s; + + } + } + + /** + * Implementation of URIHandler.ErrorEvent interface. + */ + public class URIHandlerErrorImpl implements URIHandler.ErrorEvent, + Serializable { + + private final URIHandler owner; + + private final Throwable throwable; + + /** + * + * @param owner + * @param throwable + */ + private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) { + this.owner = owner; + this.throwable = throwable; + } + + /** + * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable() + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * @see com.vaadin.terminal.URIHandler.ErrorEvent#getURIHandler() + */ + public URIHandler getURIHandler() { + return owner; + } + } + + public void requireLocale(String value) { + if (locales == null) { + locales = new ArrayList<String>(); + locales.add(application.getLocale().toString()); + pendingLocalesIndex = 0; + } + if (!locales.contains(value)) { + locales.add(value); + } + } + + private Locale generateLocale(String value) { + final String[] temp = value.split("_"); + if (temp.length == 1) { + return new Locale(temp[0]); + } else if (temp.length == 2) { + return new Locale(temp[0], temp[1]); + } else { + return new Locale(temp[0], temp[1], temp[2]); + } + } + + /* + * Upload progress listener notifies upload component once when Jakarta + * FileUpload can determine content length. Used to detect files total size, + * uploads progress can be tracked inside upload. + */ + private class UploadProgressListener implements ProgressListener, + Serializable { + + Upload uploadComponent; + + boolean updated = false; + + public void setUpload(Upload u) { + uploadComponent = u; + } + + public void update(long bytesRead, long contentLength, int items) { + if (!updated && uploadComponent != null) { + uploadComponent.setUploadSize(contentLength); + updated = true; + } + } + } + + /** + * Helper method to test if a component contains another + * + * @param parent + * @param child + */ + private static boolean isChildOf(Component parent, Component child) { + Component p = child.getParent(); + while (p != null) { + if (parent == p) { + return true; + } + p = p.getParent(); + } + return false; + } + + private class InvalidUIDLSecurityKeyException extends + GeneralSecurityException { + + InvalidUIDLSecurityKeyException(String message) { + super(message); + } + + } + + /** + * Handles the requested URI. An application can add handlers to do special + * processing, when a certain URI is requested. The handlers are invoked + * before any windows URIs are processed and if a DownloadStream is returned + * it is sent to the client. + * + * @param application + * the Application owning the URI. + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @return boolean <code>true</code> if the request was handled and further + * processing should be suppressed, <code>false</code> otherwise. + * @see com.vaadin.terminal.URIHandler + */ + DownloadStream handleURI(Window window, HttpServletRequest request, + HttpServletResponse response) { + + String uri = applicationServlet.getRequestPathInfo(request); + + // If no URI is available + if (uri == null) { + uri = ""; + } else { + // Removes the leading / + while (uri.startsWith("/") && uri.length() > 0) { + uri = uri.substring(1); + } + } + + // Handles the uri + try { + URL context = application.getURL(); + if (window == application.getMainWindow()) { + DownloadStream stream = null; + /* + * Application.handleURI run first. Handles possible + * ApplicationResources. + */ + stream = application.handleURI(context, uri); + if (stream == null) { + stream = window.handleURI(context, uri); + } + return stream; + } else { + // Resolve the prefix end inded + final int index = uri.indexOf('/'); + if (index > 0) { + String prefix = uri.substring(0, index); + URL windowContext; + windowContext = new URL(context, prefix + "/"); + final String windowUri = (uri.length() > prefix.length() + 1) ? uri + .substring(prefix.length() + 1) + : ""; + return window.handleURI(windowContext, windowUri); + } else { + return null; + } + } + + } catch (final Throwable t) { + application.getErrorHandler().terminalError( + new URIHandlerErrorImpl(application, t)); + return null; + } + } +} |