/*
* Copyright 2000-2013 Vaadin Ltd.
*
* 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 com.vaadin.server;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
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.io.StringWriter;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
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 java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.vaadin.annotations.JavaScript;
import com.vaadin.annotations.PreserveOnRefresh;
import com.vaadin.annotations.StyleSheet;
import com.vaadin.server.ClientConnector.ConnectorErrorEvent;
import com.vaadin.server.ComponentSizeValidator.InvalidLayout;
import com.vaadin.server.ServerRpcManager.RpcInvocationException;
import com.vaadin.server.StreamVariable.StreamingEndEvent;
import com.vaadin.server.StreamVariable.StreamingErrorEvent;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector;
import com.vaadin.shared.JavaScriptConnectorState;
import com.vaadin.shared.Version;
import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
import com.vaadin.shared.communication.MethodInvocation;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.communication.SharedState;
import com.vaadin.shared.communication.UidlValue;
import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.ui.Component;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.ui.HasComponents;
import com.vaadin.ui.LegacyComponent;
import com.vaadin.ui.SelectiveRenderer;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
/**
* This is a common base class for the server-side implementations of the
* communication system between the client code (compiled with GWT into
* JavaScript) and the server side components. Its client side counterpart is
* {@link com.vaadin.client.ApplicationConnection}.
* <p>
* TODO Document better!
*
* @deprecated As of 7.0. Will likely change or be removed in a future version
*/
@Deprecated
@SuppressWarnings("serial")
public abstract class AbstractCommunicationManager implements Serializable {
private static final String DASHDASH = "--";
private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler();
private static final RequestHandler CONNECTOR_RESOURCE_HANDLER = new ConnectorResourceHandler();
/**
* TODO Document me!
*
* @author peholmst
*
* @deprecated As of 7.0. Will likely change or be removed in a future
* version
*/
@Deprecated
public interface Callback extends Serializable {
public void criticalNotification(VaadinRequest request,
VaadinResponse response, String cap, String msg,
String details, String outOfSyncURL) throws IOException;
}
static class UploadInterruptedException extends Exception {
public UploadInterruptedException() {
super("Upload interrupted by other thread");
}
}
// flag used in the request to indicate that the security token should be
// written to the response
private static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken";
/* Variable records indexes */
public static final char VAR_BURST_SEPARATOR = '\u001d';
public static final char VAR_ESCAPE_CHARACTER = '\u001b';
private final HashMap<Integer, ClientCache> uiToClientCache = new HashMap<Integer, ClientCache>();
private static final int MAX_BUFFER_SIZE = 64 * 1024;
/* Same as in apache commons file upload library that was previously used. */
private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024;
/**
* The session this communication manager is used for
*/
private final VaadinSession session;
private List<String> locales;
private int pendingLocalesIndex;
private int timeoutInterval = -1;
private DragAndDropService dragAndDropService;
private String requestThemeName;
private int maxInactiveInterval;
private ClientConnector highlightedConnector;
private Map<String, Class<?>> publishedFileContexts = new HashMap<String, Class<?>>();
/**
* TODO New constructor - document me!
*
* @param session
*/
public AbstractCommunicationManager(VaadinSession session) {
this.session = session;
session.addRequestHandler(getBootstrapHandler());
session.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER);
session.addRequestHandler(CONNECTOR_RESOURCE_HANDLER);
requireLocale(session.getLocale().toString());
}
protected VaadinSession getSession() {
return session;
}
private static final int LF = "\n".getBytes()[0];
private static final String CRLF = "\r\n";
private static final String UTF8 = "UTF-8";
private static String readLine(InputStream stream) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
int readByte = stream.read();
while (readByte != LF) {
bout.write(readByte);
readByte = stream.read();
}
byte[] bytes = bout.toByteArray();
return new String(bytes, 0, bytes.length - 1, UTF8);
}
/**
* Method used to stream content from a multipart request (either from
* servlet or portlet request) to given StreamVariable
*
*
* @param request
* @param response
* @param streamVariable
* @param owner
* @param boundary
* @throws IOException
*/
protected void doHandleSimpleMultipartFileUpload(VaadinRequest request,
VaadinResponse response, StreamVariable streamVariable,
String variableName, ClientConnector owner, String boundary)
throws IOException {
// multipart parsing, supports only one file for request, but that is
// fine for our current terminal
final InputStream inputStream = request.getInputStream();
int contentLength = request.getContentLength();
boolean atStart = false;
boolean firstFileFieldFound = false;
String rawfilename = "unknown";
String rawMimeType = "application/octet-stream";
/*
* Read the stream until the actual file starts (empty line). Read
* filename and content type from multipart headers.
*/
while (!atStart) {
String readLine = readLine(inputStream);
contentLength -= (readLine.getBytes(UTF8).length + CRLF.length());
if (readLine.startsWith("Content-Disposition:")
&& readLine.indexOf("filename=") > 0) {
rawfilename = readLine.replaceAll(".*filename=", "");
char quote = rawfilename.charAt(0);
rawfilename = rawfilename.substring(1);
rawfilename = rawfilename.substring(0,
rawfilename.indexOf(quote));
firstFileFieldFound = true;
} else if (firstFileFieldFound && readLine.equals("")) {
atStart = true;
} else if (readLine.startsWith("Content-Type")) {
rawMimeType = readLine.split(": ")[1];
}
}
contentLength -= (boundary.length() + CRLF.length() + 2
* DASHDASH.length() + CRLF.length());
/*
* Reads bytes from the underlying stream. Compares the read bytes to
* the boundary string and returns -1 if met.
*
* The matching happens so that if the read byte equals to the first
* char of boundary string, the stream goes to "buffering mode". In
* buffering mode bytes are read until the character does not match the
* corresponding from boundary string or the full boundary string is
* found.
*
* Note, if this is someday needed elsewhere, don't shoot yourself to
* foot and split to a top level helper class.
*/
InputStream simpleMultiPartReader = new SimpleMultiPartInputStream(
inputStream, boundary);
/*
* Should report only the filename even if the browser sends the path
*/
final String filename = removePath(rawfilename);
final String mimeType = rawMimeType;
try {
// TODO Shouldn't this check connectorEnabled?
if (owner == null) {
throw new UploadException(
"File upload ignored because the connector for the stream variable was not found");
}
if (owner instanceof Component) {
if (((Component) owner).isReadOnly()) {
throw new UploadException(
"Warning: file upload ignored because the componente was read-only");
}
}
boolean forgetVariable = streamToReceiver(simpleMultiPartReader,
streamVariable, filename, mimeType, contentLength);
if (forgetVariable) {
cleanStreamVariable(owner, variableName);
}
} catch (Exception e) {
session.lock();
try {
handleConnectorRelatedException(owner, e);
} finally {
session.unlock();
}
}
sendUploadResponse(request, response);
}
/**
* Used to stream plain file post (aka XHR2.post(File))
*
* @param request
* @param response
* @param streamVariable
* @param owner
* @param contentLength
* @throws IOException
*/
protected void doHandleXhrFilePost(VaadinRequest request,
VaadinResponse response, StreamVariable streamVariable,
String variableName, ClientConnector owner, int contentLength)
throws IOException {
// These are unknown in filexhr ATM, maybe add to Accept header that
// is accessible in portlets
final String filename = "unknown";
final String mimeType = filename;
final InputStream stream = request.getInputStream();
try {
/*
* safe cast as in GWT terminal all variable owners are expected to
* be components.
*/
Component component = (Component) owner;
if (component.isReadOnly()) {
throw new UploadException(
"Warning: file upload ignored because the component was read-only");
}
boolean forgetVariable = streamToReceiver(stream, streamVariable,
filename, mimeType, contentLength);
if (forgetVariable) {
cleanStreamVariable(owner, variableName);
}
} catch (Exception e) {
session.lock();
try {
handleConnectorRelatedException(owner, e);
} finally {
session.unlock();
}
}
sendUploadResponse(request, response);
}
/**
* @param in
* @param streamVariable
* @param filename
* @param type
* @param contentLength
* @return true if the streamvariable has informed that the terminal can
* forget this variable
* @throws UploadException
*/
protected final boolean streamToReceiver(final InputStream in,
StreamVariable streamVariable, String filename, String type,
int contentLength) throws UploadException {
if (streamVariable == null) {
throw new IllegalStateException(
"StreamVariable for the post not found");
}
final VaadinSession session = getSession();
OutputStream out = null;
int totalBytes = 0;
StreamingStartEventImpl startedEvent = new StreamingStartEventImpl(
filename, type, contentLength);
try {
boolean listenProgress;
session.lock();
try {
streamVariable.streamingStarted(startedEvent);
out = streamVariable.getOutputStream();
listenProgress = streamVariable.listenProgress();
} finally {
session.unlock();
}
// Gets the output target stream
if (out == null) {
throw new NoOutputStreamException();
}
if (null == in) {
// No file, for instance non-existent filename in html upload
throw new NoInputStreamException();
}
final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE];
int bytesReadToBuffer = 0;
while ((bytesReadToBuffer = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesReadToBuffer);
totalBytes += bytesReadToBuffer;
if (listenProgress) {
// update progress if listener set and contentLength
// received
session.lock();
try {
StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl(
filename, type, contentLength, totalBytes);
streamVariable.onProgress(progressEvent);
} finally {
session.unlock();
}
}
if (streamVariable.isInterrupted()) {
throw new UploadInterruptedException();
}
}
// upload successful
out.close();
StreamingEndEvent event = new StreamingEndEventImpl(filename, type,
totalBytes);
session.lock();
try {
streamVariable.streamingFinished(event);
} finally {
session.unlock();
}
} catch (UploadInterruptedException e) {
// Download interrupted by application code
tryToCloseStream(out);
StreamingErrorEvent event = new StreamingErrorEventImpl(filename,
type, contentLength, totalBytes, e);
session.lock();
try {
streamVariable.streamingFailed(event);
} finally {
session.unlock();
}
// Note, we are not throwing interrupted exception forward as it is
// not a terminal level error like all other exception.
} catch (final Exception e) {
tryToCloseStream(out);
session.lock();
try {
StreamingErrorEvent event = new StreamingErrorEventImpl(
filename, type, contentLength, totalBytes, e);
streamVariable.streamingFailed(event);
// throw exception for terminal to be handled (to be passed to
// terminalErrorHandler)
throw new UploadException(e);
} finally {
session.unlock();
}
}
return startedEvent.isDisposed();
}
static void tryToCloseStream(OutputStream out) {
try {
// try to close output stream (e.g. file handle)
if (out != null) {
out.close();
}
} catch (IOException e1) {
// NOP
}
}
/**
* Removes any possible path information from the filename and returns the
* filename. Separators / and \\ are used.
*
* @param name
* @return
*/
private static String removePath(String filename) {
if (filename != null) {
filename = filename.replaceAll("^.*[/\\\\]", "");
}
return filename;
}
/**
* TODO document
*
* @param request
* @param response
* @throws IOException
*/
protected void sendUploadResponse(VaadinRequest request,
VaadinResponse response) throws IOException {
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();
}
/**
* Internally process a UIDL request from the client.
*
* This method calls
* {@link #handleVariables(VaadinRequest, VaadinResponse, Callback, VaadinSession, UI)}
* to process any changes to variables by the client and then repaints
* affected components using {@link #paintAfterVariableChanges()}.
*
* Also, some cleanup is done when a request arrives for an session that has
* already been closed.
*
* The method handleUidlRequest(...) in subclasses should call this method.
*
* TODO better documentation
*
* @param request
* @param response
* @param callback
* @param uI
* target window for the UIDL request, can be null if target not
* found
* @throws IOException
* @throws InvalidUIDLSecurityKeyException
* @throws JSONException
*/
public void handleUidlRequest(VaadinRequest request,
VaadinResponse response, Callback callback, UI uI)
throws IOException, InvalidUIDLSecurityKeyException, JSONException {
checkWidgetsetVersion(request);
requestThemeName = request.getParameter("theme");
maxInactiveInterval = request.getWrappedSession()
.getMaxInactiveInterval();
// repaint requested or session has timed out and new one is created
boolean repaintAll;
final OutputStream out;
repaintAll = (request
.getParameter(ApplicationConstants.URL_PARAMETER_REPAINT_ALL) != null);
// || (request.getSession().isNew()); FIXME What the h*ll is this??
out = response.getOutputStream();
boolean analyzeLayouts = false;
if (repaintAll) {
// analyzing can be done only with repaintAll
analyzeLayouts = (request
.getParameter(ApplicationConstants.PARAM_ANALYZE_LAYOUTS) != null);
String pid = request
.getParameter(ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR);
if (pid != null) {
highlightedConnector = uI.getConnectorTracker().getConnector(
pid);
highlightConnector(highlightedConnector);
}
}
final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(out, "UTF-8")));
// The rest of the process is synchronized with the session
// in order to guarantee that no parallel variable handling is
// made
session.lock();
try {
// Verify that there's an UI
if (uI == null) {
// This should not happen, no windows exists but
// session is still open.
getLogger().warning("Could not get UI for session");
return;
}
session.setLastRequestTimestamp(System.currentTimeMillis());
// Change all variables based on request parameters
if (!handleVariables(request, response, callback, session, uI)) {
// var inconsistency; the client is probably out-of-sync
SystemMessages ci = response.getService().getSystemMessages(
uI.getLocale(), request);
String msg = ci.getOutOfSyncMessage();
String cap = ci.getOutOfSyncCaption();
if (msg != null || cap != null) {
callback.criticalNotification(request, response, cap, msg,
null, ci.getOutOfSyncURL());
// will reload page after this
return;
}
// No message to show, let's just repaint all.
repaintAll = true;
}
paintAfterVariableChanges(request, response, callback, repaintAll,
outWriter, uI, analyzeLayouts);
postPaint(uI);
} finally {
session.unlock();
}
outWriter.close();
requestThemeName = null;
}
/**
* Checks that the version reported by the client (widgetset) matches that
* of the server.
*
* @param request
*/
private void checkWidgetsetVersion(VaadinRequest request) {
String widgetsetVersion = request.getParameter("v-wsver");
if (widgetsetVersion == null) {
// Only check when the widgetset version is reported. It is reported
// in the first UIDL request (not the initial request as it is a
// plain GET /)
return;
}
if (!Version.getFullVersion().equals(widgetsetVersion)) {
getLogger().warning(
String.format(Constants.WIDGETSET_MISMATCH_INFO,
Version.getFullVersion(), widgetsetVersion));
}
}
/**
* Method called after the paint phase while still being synchronized on the
* session
*
* @param uI
*
*/
protected void postPaint(UI uI) {
// Remove connectors that have been detached from the session during
// handling of the request
uI.getConnectorTracker().cleanConnectorMap();
}
protected void highlightConnector(ClientConnector highlightedConnector) {
StringBuilder sb = new StringBuilder();
sb.append("*** Debug details of a connector: *** \n");
sb.append("Type: ");
sb.append(highlightedConnector.getClass().getName());
sb.append("\nId:");
sb.append(highlightedConnector.getConnectorId());
if (highlightedConnector instanceof Component) {
Component component = (Component) highlightedConnector;
if (component.getCaption() != null) {
sb.append("\nCaption:");
sb.append(component.getCaption());
}
}
printHighlightedConnectorHierarchy(sb, highlightedConnector);
getLogger().info(sb.toString());
}
protected void printHighlightedConnectorHierarchy(StringBuilder sb,
ClientConnector connector) {
LinkedList<ClientConnector> h = new LinkedList<ClientConnector>();
h.add(connector);
ClientConnector parent = connector.getParent();
while (parent != null) {
h.addFirst(parent);
parent = parent.getParent();
}
sb.append("\nConnector hierarchy:\n");
VaadinSession session2 = connector.getUI().getSession();
sb.append(session2.getClass().getName());
sb.append("(");
sb.append(session2.getClass().getSimpleName());
sb.append(".java");
sb.append(":1)");
int l = 1;
for (ClientConnector connector2 : h) {
sb.append("\n");
for (int i = 0; i < l; i++) {
sb.append(" ");
}
l++;
Class<? extends ClientConnector> connectorClass = connector2
.getClass();
Class<?> topClass = connectorClass;
while (topClass.getEnclosingClass() != null) {
topClass = topClass.getEnclosingClass();
}
sb.append(connectorClass.getName());
sb.append("(");
sb.append(topClass.getSimpleName());
sb.append(".java:1)");
}
}
/**
* TODO document
*
* @param request
* @param response
* @param callback
* @param repaintAll
* @param outWriter
* @param window
* @param analyzeLayouts
* @throws PaintException
* @throws IOException
* @throws JSONException
*/
private void paintAfterVariableChanges(VaadinRequest request,
VaadinResponse response, Callback callback, boolean repaintAll,
final PrintWriter outWriter, UI uI, boolean analyzeLayouts)
throws PaintException, IOException, JSONException {
openJsonMessage(outWriter, response);
// security key
Object writeSecurityTokenFlag = request
.getAttribute(WRITE_SECURITY_TOKEN_FLAG);
if (writeSecurityTokenFlag != null) {
outWriter.print(getSecurityKeyUIDL(request));
}
writeUidlResponse(request, repaintAll, outWriter, uI, analyzeLayouts);
closeJsonMessage(outWriter);
outWriter.close();
}
/**
* Gets the security key (and generates one if needed) as UIDL.
*
* @param request
* @return the security key UIDL or "" if the feature is turned off
*/
public String getSecurityKeyUIDL(VaadinRequest request) {
final String seckey = getSecurityKey(request);
if (seckey != null) {
return "\"" + ApplicationConstants.UIDL_SECURITY_TOKEN_ID + "\":\""
+ seckey + "\",";
} else {
return "";
}
}
/**
* Gets the security key (and generates one if needed).
*
* @param request
* @return the security key
*/
protected String getSecurityKey(VaadinRequest request) {
String seckey = null;
WrappedSession session = request.getWrappedSession();
seckey = (String) session
.getAttribute(ApplicationConstants.UIDL_SECURITY_TOKEN_ID);
if (seckey == null) {
seckey = UUID.randomUUID().toString();
session.setAttribute(ApplicationConstants.UIDL_SECURITY_TOKEN_ID,
seckey);
}
return seckey;
}
@SuppressWarnings("unchecked")
public void writeUidlResponse(VaadinRequest request, boolean repaintAll,
final PrintWriter outWriter, UI ui, boolean analyzeLayouts)
throws PaintException, JSONException {
ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>();
VaadinSession session = ui.getSession();
// Paints components
ConnectorTracker uiConnectorTracker = ui.getConnectorTracker();
getLogger().log(Level.FINE, "* Creating response to client");
if (repaintAll) {
getClientCache(ui).clear();
uiConnectorTracker.markAllConnectorsDirty();
uiConnectorTracker.markAllClientSidesUninitialized();
// Reset sent locales
locales = null;
requireLocale(session.getLocale().toString());
}
dirtyVisibleConnectors
.addAll(getDirtyVisibleConnectors(uiConnectorTracker));
getLogger().log(Level.FINE, "Found {0} dirty connectors to paint",
dirtyVisibleConnectors.size());
for (ClientConnector connector : dirtyVisibleConnectors) {
boolean initialized = uiConnectorTracker
.isClientSideInitialized(connector);
connector.beforeClientResponse(!initialized);
}
uiConnectorTracker.setWritingResponse(true);
try {
outWriter.print("\"changes\":[");
List<InvalidLayout> invalidComponentRelativeSizes = null;
JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter,
!repaintAll);
legacyPaint(paintTarget, dirtyVisibleConnectors);
if (analyzeLayouts) {
invalidComponentRelativeSizes = ComponentSizeValidator
.validateComponentRelativeSizes(ui.getContent(), null,
null);
// Also check any existing subwindows
if (ui.getWindows() != null) {
for (Window subWindow : ui.getWindows()) {
invalidComponentRelativeSizes = ComponentSizeValidator
.validateComponentRelativeSizes(
subWindow.getContent(),
invalidComponentRelativeSizes, null);
}
}
}
paintTarget.close();
outWriter.print("], "); // close changes
// send shared state to client
// for now, send the complete state of all modified and new
// components
// Ideally, all this would be sent before "changes", but that causes
// complications with legacy components that create sub-components
// in their paint phase. Nevertheless, this will be processed on the
// client after component creation but before legacy UIDL
// processing.
JSONObject sharedStates = new JSONObject();
for (ClientConnector connector : dirtyVisibleConnectors) {
// encode and send shared state
try {
JSONObject stateJson = connector.encodeState();
if (stateJson != null && stateJson.length() != 0) {
sharedStates.put(connector.getConnectorId(), stateJson);
}
} catch (JSONException e) {
throw new PaintException(
"Failed to serialize shared state for connector "
+ connector.getClass().getName() + " ("
+ connector.getConnectorId() + "): "
+ e.getMessage(), e);
}
}
outWriter.print("\"state\":");
outWriter.append(sharedStates.toString());
outWriter.print(", "); // close states
// TODO This should be optimized. The type only needs to be
// sent once for each connector id + on refresh. Use the same cache
// as
// widget mapping
JSONObject connectorTypes = new JSONObject();
for (ClientConnector connector : dirtyVisibleConnectors) {
String connectorType = paintTarget.getTag(connector);
try {
connectorTypes.put(connector.getConnectorId(),
connectorType);
} catch (JSONException e) {
throw new PaintException(
"Failed to send connector type for connector "
+ connector.getConnectorId() + ": "
+ e.getMessage(), e);
}
}
outWriter.print("\"types\":");
outWriter.append(connectorTypes.toString());
outWriter.print(", "); // close states
// Send update hierarchy information to the client.
// This could be optimized aswell to send only info if hierarchy has
// actually changed. Much like with the shared state. Note though
// that an empty hierarchy is information aswell (e.g. change from 1
// child to 0 children)
outWriter.print("\"hierarchy\":");
JSONObject hierarchyInfo = new JSONObject();
for (ClientConnector connector : dirtyVisibleConnectors) {
String connectorId = connector.getConnectorId();
JSONArray children = new JSONArray();
for (ClientConnector child : AbstractClientConnector
.getAllChildrenIterable(connector)) {
if (isConnectorVisibleToClient(child)) {
children.put(child.getConnectorId());
}
}
try {
hierarchyInfo.put(connectorId, children);
} catch (JSONException e) {
throw new PaintException(
"Failed to send hierarchy information about "
+ connectorId + " to the client: "
+ e.getMessage(), e);
}
}
outWriter.append(hierarchyInfo.toString());
outWriter.print(", "); // close hierarchy
uiConnectorTracker.markAllConnectorsClean();
// send server to client RPC calls for components in the UI, in call
// order
// collect RPC calls from components in the UI in the order in
// which they were performed, remove the calls from components
LinkedList<ClientConnector> rpcPendingQueue = new LinkedList<ClientConnector>(
dirtyVisibleConnectors);
List<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(dirtyVisibleConnectors);
JSONArray rpcCalls = new JSONArray();
for (ClientMethodInvocation invocation : pendingInvocations) {
// add invocation to rpcCalls
try {
JSONArray invocationJson = new JSONArray();
invocationJson.put(invocation.getConnector()
.getConnectorId());
invocationJson.put(invocation.getInterfaceName());
invocationJson.put(invocation.getMethodName());
JSONArray paramJson = new JSONArray();
for (int i = 0; i < invocation.getParameterTypes().length; ++i) {
Type parameterType = invocation.getParameterTypes()[i];
Object referenceParameter = null;
// TODO Use default values for RPC parameter types
// if (!JsonCodec.isInternalType(parameterType)) {
// try {
// referenceParameter = parameterType.newInstance();
// } catch (Exception e) {
// logger.log(Level.WARNING,
// "Error creating reference object for parameter of type "
// + parameterType.getName());
// }
// }
EncodeResult encodeResult = JsonCodec.encode(
invocation.getParameters()[i],
referenceParameter, parameterType,
ui.getConnectorTracker());
paramJson.put(encodeResult.getEncodedValue());
}
invocationJson.put(paramJson);
rpcCalls.put(invocationJson);
} catch (JSONException e) {
throw new PaintException(
"Failed to serialize RPC method call parameters for connector "
+ invocation.getConnector()
.getConnectorId() + " method "
+ invocation.getInterfaceName() + "."
+ invocation.getMethodName() + ": "
+ e.getMessage(), e);
}
}
if (rpcCalls.length() > 0) {
outWriter.print("\"rpc\" : ");
outWriter.append(rpcCalls.toString());
outWriter.print(", "); // close rpc
}
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("]");
}
if (highlightedConnector != null) {
outWriter.write(", \"hl\":\"");
outWriter.write(highlightedConnector.getConnectorId());
outWriter.write("\"");
highlightedConnector = null;
}
}
SystemMessages ci = request.getService().getSystemMessages(
ui.getLocale(), request);
// 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 = getTimeoutInterval();
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
// TODO We should only precache the layouts that are not
// cached already (plagiate from usedPaintableTypes)
int resourceIndex = 0;
for (final Iterator<Object> i = paintTarget.getUsedResources()
.iterator(); i.hasNext();) {
final String resource = (String) i.next();
InputStream is = null;
try {
is = getThemeResourceAsStream(ui, getTheme(ui), resource);
} catch (final Exception e) {
// FIXME: Handle exception
getLogger().log(Level.FINER,
"Failed to get theme resource stream.", e);
}
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
getLogger().log(Level.INFO, "Resource transfer failed",
e);
}
outWriter.print("\""
+ JsonPaintTarget.escapeJSON(layout.toString())
+ "\"");
} else {
// FIXME: Handle exception
getLogger().severe("CustomLayout not found: " + resource);
}
}
outWriter.print("}");
Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget
.getUsedClientConnectors();
boolean typeMappingsOpen = false;
ClientCache clientCache = getClientCache(ui);
List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>();
for (Class<? extends ClientConnector> class1 : usedClientConnectors) {
if (clientCache.cache(class1)) {
// client does not know the mapping key for this type, send
// mapping to client
newConnectorTypes.add(class1);
if (!typeMappingsOpen) {
typeMappingsOpen = true;
outWriter.print(", \"typeMappings\" : { ");
} else {
outWriter.print(" , ");
}
String canonicalName = class1.getCanonicalName();
outWriter.print("\"");
outWriter.print(canonicalName);
outWriter.print("\" : ");
outWriter.print(getTagForType(class1));
}
}
if (typeMappingsOpen) {
outWriter.print(" }");
}
boolean typeInheritanceMapOpen = false;
if (typeMappingsOpen) {
// send the whole type inheritance map if any new mappings
for (Class<? extends ClientConnector> class1 : usedClientConnectors) {
if (!ClientConnector.class.isAssignableFrom(class1
.getSuperclass())) {
continue;
}
if (!typeInheritanceMapOpen) {
typeInheritanceMapOpen = true;
outWriter.print(", \"typeInheritanceMap\" : { ");
} else {
outWriter.print(" , ");
}
outWriter.print("\"");
outWriter.print(getTagForType(class1));
outWriter.print("\" : ");
outWriter
.print(getTagForType((Class<? extends ClientConnector>) class1
.getSuperclass()));
}
if (typeInheritanceMapOpen) {
outWriter.print(" }");
}
}
/*
* Ensure super classes come before sub classes to get script
* dependency order right. Sub class @JavaScript might assume that
*
* @JavaScript defined by super class is already loaded.
*/
Collections.sort(newConnectorTypes, new Comparator<Class<?>>() {
@Override
public int compare(Class<?> o1, Class<?> o2) {
// TODO optimize using Class.isAssignableFrom?
return hierarchyDepth(o1) - hierarchyDepth(o2);
}
private int hierarchyDepth(Class<?> type) {
if (type == Object.class) {
return 0;
} else {
return hierarchyDepth(type.getSuperclass()) + 1;
}
}
});
List<String> scriptDependencies = new ArrayList<String>();
List<String> styleDependencies = new ArrayList<String>();
for (Class<? extends ClientConnector> class1 : newConnectorTypes) {
JavaScript jsAnnotation = class1
.getAnnotation(JavaScript.class);
if (jsAnnotation != null) {
for (String uri : jsAnnotation.value()) {
scriptDependencies.add(registerDependency(uri, class1));
}
}
StyleSheet styleAnnotation = class1
.getAnnotation(StyleSheet.class);
if (styleAnnotation != null) {
for (String uri : styleAnnotation.value()) {
styleDependencies.add(registerDependency(uri, class1));
}
}
}
// Include script dependencies in output if there are any
if (!scriptDependencies.isEmpty()) {
outWriter.print(", \"scriptDependencies\": "
+ new JSONArray(scriptDependencies).toString());
}
// Include style dependencies in output if there are any
if (!styleDependencies.isEmpty()) {
outWriter.print(", \"styleDependencies\": "
+ new JSONArray(styleDependencies).toString());
}
// add any pending locale definitions requested by the client
printLocaleDeclarations(outWriter);
if (dragAndDropService != null) {
dragAndDropService.printJSONResponse(outWriter);
}
for (ClientConnector connector : dirtyVisibleConnectors) {
uiConnectorTracker.markClientSideInitialized(connector);
}
assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended.";
writePerformanceData(outWriter);
} finally {
uiConnectorTracker.setWritingResponse(false);
}
}
public static JSONObject encodeState(ClientConnector connector,
SharedState state) throws JSONException {
UI uI = connector.getUI();
ConnectorTracker connectorTracker = uI.getConnectorTracker();
Class<? extends SharedState> stateType = connector.getStateType();
Object diffState = connectorTracker.getDiffState(connector);
boolean supportsDiffState = !JavaScriptConnectorState.class
.isAssignableFrom(stateType);
if (diffState == null && supportsDiffState) {
// Use an empty state object as reference for full
// repaints
try {
SharedState referenceState = stateType.newInstance();
EncodeResult encodeResult = JsonCodec.encode(referenceState,
null, stateType, uI.getConnectorTracker());
diffState = encodeResult.getEncodedValue();
} catch (Exception e) {
getLogger()
.log(Level.WARNING,
"Error creating reference object for state of type {0}",
stateType.getName());
}
}
EncodeResult encodeResult = JsonCodec.encode(state, diffState,
stateType, uI.getConnectorTracker());
if (supportsDiffState) {
connectorTracker.setDiffState(connector,
(JSONObject) encodeResult.getEncodedValue());
}
return (JSONObject) encodeResult.getDiff();
}
/**
* Resolves a dependency URI, registering the URI with this
* {@code AbstractCommunicationManager} if needed and returns a fully
* qualified URI.
*/
private String registerDependency(String resourceUri, Class<?> context) {
try {
URI uri = new URI(resourceUri);
String protocol = uri.getScheme();
if (ApplicationConstants.PUBLISHED_PROTOCOL_NAME.equals(protocol)) {
// Strip initial slash
String resourceName = uri.getPath().substring(1);
return registerPublishedFile(resourceName, context);
}
if (protocol != null || uri.getHost() != null) {
return resourceUri;
}
// Bare path interpreted as published file
return registerPublishedFile(resourceUri, context);
} catch (URISyntaxException e) {
getLogger().log(Level.WARNING,
"Could not parse resource url " + resourceUri, e);
return resourceUri;
}
}
private String registerPublishedFile(String name, Class<?> context) {
synchronized (publishedFileContexts) {
// Add to map of names accepted by servePublishedFile
if (publishedFileContexts.containsKey(name)) {
Class<?> oldContext = publishedFileContexts.get(name);
if (oldContext != context) {
getLogger()
.log(Level.WARNING,
"{0} published by both {1} and {2}. File from {2} will be used.",
new Object[] { name, context, oldContext });
}
} else {
publishedFileContexts.put(name, context);
}
}
return ApplicationConstants.PUBLISHED_PROTOCOL_PREFIX + "/" + name;
}
/**
* Adds the performance timing data (used by TestBench 3) to the UIDL
* response.
*/
private void writePerformanceData(final PrintWriter outWriter) {
outWriter.write(String.format(", \"timings\":[%d, %d]",
session.getCumulativeRequestDuration(),
session.getLastRequestDuration()));
}
private void legacyPaint(PaintTarget paintTarget,
ArrayList<ClientConnector> dirtyVisibleConnectors)
throws PaintException {
List<LegacyComponent> legacyComponents = new ArrayList<LegacyComponent>();
for (Connector connector : dirtyVisibleConnectors) {
// All Components that want to use paintContent must implement
// LegacyComponent
if (connector instanceof LegacyComponent) {
legacyComponents.add((LegacyComponent) connector);
}
}
sortByHierarchy((List) legacyComponents);
for (LegacyComponent c : legacyComponents) {
if (getLogger().isLoggable(Level.FINE)) {
getLogger().log(
Level.FINE,
"Painting LegacyComponent {0}@{1}",
new Object[] { c.getClass().getName(),
Integer.toHexString(c.hashCode()) });
}
paintTarget.startTag("change");
final String pid = c.getConnectorId();
paintTarget.addAttribute("pid", pid);
LegacyPaint.paint(c, paintTarget);
paintTarget.endTag("change");
}
}
private void sortByHierarchy(List<Component> paintables) {
// Vaadin 6 requires parents to be painted before children as component
// containers rely on that their updateFromUIDL method has been called
// before children start calling e.g. updateCaption
Collections.sort(paintables, new Comparator<Component>() {
@Override
public int compare(Component c1, Component c2) {
int depth1 = 0;
while (c1.getParent() != null) {
depth1++;
c1 = c1.getParent();
}
int depth2 = 0;
while (c2.getParent() != null) {
depth2++;
c2 = c2.getParent();
}
if (depth1 < depth2) {
return -1;
}
if (depth1 > depth2) {
return 1;
}
return 0;
}
});
}
private ClientCache getClientCache(UI uI) {
Integer uiId = Integer.valueOf(uI.getUIId());
ClientCache cache = uiToClientCache.get(uiId);
if (cache == null) {
cache = new ClientCache();
uiToClientCache.put(uiId, cache);
}
return cache;
}
/**
* Checks if the connector is visible in context. For Components,
* {@link #isComponentVisibleToClient(Component)} is used. For other types
* of connectors, the contextual visibility of its first Component ancestor
* is used. If no Component ancestor is found, the connector is not visible.
*
* @param connector
* The connector to check
* @return <code>true</code> if the connector is visible to the client,
* <code>false</code> otherwise
*/
public static boolean isConnectorVisibleToClient(ClientConnector connector) {
if (connector instanceof Component) {
return isComponentVisibleToClient((Component) connector);
} else {
ClientConnector parent = connector.getParent();
if (parent == null) {
return false;
} else {
return isConnectorVisibleToClient(parent);
}
}
}
/**
* Checks if the component should be visible to the client. Returns false if
* the child should not be sent to the client, true otherwise.
*
* @param child
* The child to check
* @return true if the child is visible to the client, false otherwise
*/
public static boolean isComponentVisibleToClient(Component child) {
if (!child.isVisible()) {
return false;
}
HasComponents parent = child.getParent();
if (parent instanceof SelectiveRenderer) {
if (!((SelectiveRenderer) parent).isRendered(child)) {
return false;
}
}
if (parent != null) {
return isComponentVisibleToClient(parent);
} else {
if (child instanceof UI) {
// UI has no parent and visibility was checked above
return true;
} else {
// Component which is not attached to any UI
return false;
}
}
}
private static class NullIterator<E> implements Iterator<E> {
@Override
public boolean hasNext() {
return false;
}
@Override
public E next() {
return null;
}
@Override
public void remove() {
}
}
/**
* Collects all pending RPC calls from listed {@link ClientConnector}s and
* clears their RPC queues.
*
* @param rpcPendingQueue
* list of {@link ClientConnector} of interest
* @return ordered list of pending RPC calls
*/
private List<ClientMethodInvocation> collectPendingRpcCalls(
List<ClientConnector> rpcPendingQueue) {
List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>();
for (ClientConnector connector : rpcPendingQueue) {
List<ClientMethodInvocation> paintablePendingRpc = connector
.retrievePendingRpcCalls();
if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) {
List<ClientMethodInvocation> oldPendingRpc = pendingInvocations;
int totalCalls = pendingInvocations.size()
+ paintablePendingRpc.size();
pendingInvocations = new ArrayList<ClientMethodInvocation>(
totalCalls);
// merge two ordered comparable lists
for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) {
if (paintableIndex >= paintablePendingRpc.size()
|| (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc
.get(oldIndex))
.compareTo(paintablePendingRpc
.get(paintableIndex)) <= 0)) {
pendingInvocations.add(oldPendingRpc.get(oldIndex++));
} else {
pendingInvocations.add(paintablePendingRpc
.get(paintableIndex++));
}
}
}
}
return pendingInvocations;
}
protected abstract InputStream getThemeResourceAsStream(UI uI,
String themeName, String resource);
private int getTimeoutInterval() {
return maxInactiveInterval;
}
private String getTheme(UI uI) {
String themeName = uI.getTheme();
String requestThemeName = getRequestTheme();
if (requestThemeName != null) {
themeName = requestThemeName;
}
if (themeName == null) {
themeName = VaadinServlet.getDefaultTheme();
}
return themeName;
}
private String getRequestTheme() {
return requestThemeName;
}
/**
* Returns false if the cross site request forgery protection is turned off.
*
* @param session
* @return false if the XSRF is turned off, true otherwise
*/
public boolean isXSRFEnabled(VaadinSession session) {
return session.getConfiguration().isXsrfProtectionEnabled();
}
/**
* TODO document
*
* 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
*
* @return true if successful, false if there was an inconsistency
*/
private boolean handleVariables(VaadinRequest request,
VaadinResponse response, Callback callback, VaadinSession session,
UI uI) throws IOException, InvalidUIDLSecurityKeyException,
JSONException {
boolean success = true;
String changes = getRequestPayload(request);
if (changes != null) {
// Manage bursts one by one
final String[] bursts = changes.split(String
.valueOf(VAR_BURST_SEPARATOR));
// Security: double cookie submission pattern unless disabled by
// property
if (isXSRFEnabled(session)) {
if (bursts.length == 1 && "init".equals(bursts[0])) {
// init request; don't handle any variables, key sent in
// response.
request.setAttribute(WRITE_SECURITY_TOKEN_FLAG, true);
return true;
} else {
// ApplicationServlet has stored the security token in the
// session; check that it matched the one sent in the UIDL
String sessId = (String) request
.getWrappedSession()
.getAttribute(
ApplicationConstants.UIDL_SECURITY_TOKEN_ID);
if (sessId == null || !sessId.equals(bursts[0])) {
throw new InvalidUIDLSecurityKeyException(
"Security key mismatch");
}
}
}
for (int bi = 1; bi < bursts.length; bi++) {
// unescape any encoded separator characters in the burst
final String burst = unescapeBurst(bursts[bi]);
success &= handleBurst(request, uI, burst);
// 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());
paintAfterVariableChanges(request, response, callback,
true, outWriter, uI, false);
}
}
}
/*
* Note that we ignore inconsistencies while handling unload request.
* The client can't remove invalid variable changes from the burst, and
* we don't have the required logic implemented on the server side. E.g.
* a component is removed in a previous burst.
*/
return success;
}
/**
* Processes a message burst received from the client.
*
* A burst can contain any number of RPC calls, including legacy variable
* change calls that are processed separately.
*
* Consecutive changes to the value of the same variable are combined and
* changeVariables() is only called once for them. This preserves the Vaadin
* 6 semantics for components and add-ons that do not use Vaadin 7 RPC
* directly.
*
* @param source
* @param uI
* the UI receiving the burst
* @param burst
* the content of the burst as a String to be parsed
* @return true if the processing of the burst was successful and there were
* no messages to non-existent components
*/
public boolean handleBurst(VaadinRequest source, UI uI, final String burst) {
boolean success = true;
try {
Set<Connector> enabledConnectors = new HashSet<Connector>();
List<MethodInvocation> invocations = parseInvocations(
uI.getConnectorTracker(), burst);
for (MethodInvocation invocation : invocations) {
final ClientConnector connector = getConnector(uI,
invocation.getConnectorId());
if (connector != null && connector.isConnectorEnabled()) {
enabledConnectors.add(connector);
}
}
for (int i = 0; i < invocations.size(); i++) {
MethodInvocation invocation = invocations.get(i);
final ClientConnector connector = getConnector(uI,
invocation.getConnectorId());
if (connector == null) {
getLogger()
.log(Level.WARNING,
"Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})",
new Object[] { invocation.getConnectorId(),
invocation.getInterfaceName(),
invocation.getMethodName() });
continue;
}
if (!enabledConnectors.contains(connector)) {
if (invocation instanceof LegacyChangeVariablesInvocation) {
LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
// TODO convert window close to a separate RPC call and
// handle above - not a variable change
// Handle special case where window-close is called
// after the window has been removed from the
// application or the application has closed
Map<String, Object> changes = legacyInvocation
.getVariableChanges();
if (changes.size() == 1 && changes.containsKey("close")
&& Boolean.TRUE.equals(changes.get("close"))) {
// Silently ignore this
continue;
}
}
// Connector is disabled, log a warning and move to the next
String msg = "Ignoring RPC call for disabled connector "
+ connector.getClass().getName();
if (connector instanceof Component) {
String caption = ((Component) connector).getCaption();
if (caption != null) {
msg += ", caption=" + caption;
}
}
getLogger().warning(msg);
continue;
}
if (invocation instanceof ServerRpcMethodInvocation) {
try {
ServerRpcManager.applyInvocation(connector,
(ServerRpcMethodInvocation) invocation);
} catch (RpcInvocationException e) {
handleConnectorRelatedException(connector, e);
}
} else {
// All code below is for legacy variable changes
LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
Map<String, Object> changes = legacyInvocation
.getVariableChanges();
try {
if (connector instanceof VariableOwner) {
changeVariables(source, (VariableOwner) connector,
changes);
} else {
throw new IllegalStateException(
"Received legacy variable change for "
+ connector.getClass().getName()
+ " ("
+ connector.getConnectorId()
+ ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: "
+ changes.keySet());
}
} catch (Exception e) {
handleConnectorRelatedException(connector, e);
}
}
}
} catch (JSONException e) {
getLogger().log(Level.WARNING,
"Unable to parse RPC call from the client: {0}",
e.getMessage());
// TODO or return success = false?
throw new RuntimeException(e);
}
return success;
}
/**
* Handles an exception that occurred when processing Rpc calls or a file
* upload.
*
* @param ui
* The UI where the exception occured
* @param throwable
* The exception
* @param connector
* The Rpc target
*/
private void handleConnectorRelatedException(ClientConnector connector,
Throwable throwable) {
ErrorEvent errorEvent = new ConnectorErrorEvent(connector, throwable);
ErrorHandler handler = ErrorEvent.findErrorHandler(connector);
handler.error(errorEvent);
}
/**
* Parse a message burst from the client into a list of MethodInvocation
* instances.
*
* @param connectorTracker
* The ConnectorTracker used to lookup connectors
* @param burst
* message string (JSON)
* @return list of MethodInvocation to perform
* @throws JSONException
*/
private List<MethodInvocation> parseInvocations(
ConnectorTracker connectorTracker, final String burst)
throws JSONException {
JSONArray invocationsJson = new JSONArray(burst);
ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>();
MethodInvocation previousInvocation = null;
// parse JSON to MethodInvocations
for (int i = 0; i < invocationsJson.length(); ++i) {
JSONArray invocationJson = invocationsJson.getJSONArray(i);
MethodInvocation invocation = parseInvocation(invocationJson,
previousInvocation, connectorTracker);
if (invocation != null) {
// Can be null if the invocation was a legacy invocation and it
// was merged with the previous one or if the invocation was
// rejected because of an error.
invocations.add(invocation);
previousInvocation = invocation;
}
}
return invocations;
}
private MethodInvocation parseInvocation(JSONArray invocationJson,
MethodInvocation previousInvocation,
ConnectorTracker connectorTracker) throws JSONException {
String connectorId = invocationJson.getString(0);
String interfaceName = invocationJson.getString(1);
String methodName = invocationJson.getString(2);
if (connectorTracker.getConnector(connectorId) == null
&& !connectorId
.equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) {
getLogger()
.log(Level.WARNING,
"RPC call to "
+ interfaceName
+ "."
+ methodName
+ " received for connector "
+ connectorId
+ " but no such connector could be found. Resynchronizing client.");
// This is likely an out of sync issue (client tries to update a
// connector which is not present). Force resync.
connectorTracker.markAllConnectorsDirty();
return null;
}
JSONArray parametersJson = invocationJson.getJSONArray(3);
if (LegacyChangeVariablesInvocation.isLegacyVariableChange(
interfaceName, methodName)) {
if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) {
previousInvocation = null;
}
return parseLegacyChangeVariablesInvocation(connectorId,
interfaceName, methodName,
(LegacyChangeVariablesInvocation) previousInvocation,
parametersJson, connectorTracker);
} else {
return parseServerRpcInvocation(connectorId, interfaceName,
methodName, parametersJson, connectorTracker);
}
}
private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(
String connectorId, String interfaceName, String methodName,
LegacyChangeVariablesInvocation previousInvocation,
JSONArray parametersJson, ConnectorTracker connectorTracker)
throws JSONException {
if (parametersJson.length() != 2) {
throw new JSONException(
"Invalid parameters in legacy change variables call. Expected 2, was "
+ parametersJson.length());
}
String variableName = parametersJson.getString(0);
UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType(
UidlValue.class, true, parametersJson.get(1), connectorTracker);
Object value = uidlValue.getValue();
if (previousInvocation != null
&& previousInvocation.getConnectorId().equals(connectorId)) {
previousInvocation.setVariableChange(variableName, value);
return null;
} else {
return new LegacyChangeVariablesInvocation(connectorId,
variableName, value);
}
}
private ServerRpcMethodInvocation parseServerRpcInvocation(
String connectorId, String interfaceName, String methodName,
JSONArray parametersJson, ConnectorTracker connectorTracker)
throws JSONException {
ClientConnector connector = connectorTracker.getConnector(connectorId);
ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName);
if (rpcManager == null) {
/*
* Security: Don't even decode the json parameters if no RpcManager
* corresponding to the received method invocation has been
* registered.
*/
getLogger()
.log(Level.WARNING,
"Ignoring RPC call to {0}.{1} in connector {2} ({3}) as no RPC implementation is regsitered",
new Object[] { interfaceName, methodName,
connector.getClass().getName(), connectorId });
return null;
}
// Use interface from RpcManager instead of loading the class based on
// the string name to avoid problems with OSGi
Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface();
ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
connectorId, rpcInterface, methodName, parametersJson.length());
Object[] parameters = new Object[parametersJson.length()];
Type[] declaredRpcMethodParameterTypes = invocation.getMethod()
.getGenericParameterTypes();
for (int j = 0; j < parametersJson.length(); ++j) {
Object parameterValue = parametersJson.get(j);
Type parameterType = declaredRpcMethodParameterTypes[j];
parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType,
parameterValue, connectorTracker);
}
invocation.setParameters(parameters);
return invocation;
}
protected void changeVariables(Object source, final VariableOwner owner,
Map<String, Object> m) {
owner.changeVariables(source, m);
}
protected ClientConnector getConnector(UI uI, String connectorId) {
ClientConnector c = uI.getConnectorTracker().getConnector(connectorId);
if (c == null
&& connectorId.equals(getDragAndDropService().getConnectorId())) {
return getDragAndDropService();
}
return c;
}
private DragAndDropService getDragAndDropService() {
if (dragAndDropService == null) {
dragAndDropService = new DragAndDropService(this);
}
return dragAndDropService;
}
/**
* Reads the request data from the Request and returns it converted to an
* UTF-8 string.
*
* @param request
* @return
* @throws IOException
*/
protected String getRequestPayload(VaadinRequest request)
throws IOException {
int requestLength = request.getContentLength();
if (requestLength == 0) {
return null;
}
ByteArrayOutputStream bout = requestLength <= 0 ? new ByteArrayOutputStream()
: new ByteArrayOutputStream(requestLength);
InputStream inputStream = request.getInputStream();
byte[] buffer = new byte[MAX_BUFFER_SIZE];
while (true) {
int read = inputStream.read(buffer);
if (read == -1) {
break;
}
bout.write(buffer, 0, read);
}
String result = new String(bout.toByteArray(), "utf-8");
return result;
}
/**
* Unescape encoded burst separator characters in a burst received from the
* client. This protects from separator injection attacks.
*
* @param encodedValue
* to decode
* @return decoded value
*/
protected String unescapeBurst(String encodedValue) {
final StringBuilder result = new StringBuilder();
final StringCharacterIterator iterator = new StringCharacterIterator(
encodedValue);
char character = iterator.current();
while (character != CharacterIterator.DONE) {
if (VAR_ESCAPE_CHARACTER == character) {
character = iterator.next();
switch (character) {
case VAR_ESCAPE_CHARACTER + 0x30:
// escaped escape character
result.append(VAR_ESCAPE_CHARACTER);
break;
case VAR_BURST_SEPARATOR + 0x30:
// +0x30 makes these letters for easier reading
result.append((char) (character - 0x30));
break;
case CharacterIterator.DONE:
// error
throw new RuntimeException(
"Communication error: Unexpected end of message");
default:
// other escaped character - probably a client-server
// version mismatch
throw new RuntimeException(
"Invalid escaped character from the client - check that the widgetset and server versions match");
}
} else {
// not a special character - add it to the result as is
result.append(character);
}
character = iterator.next();
}
return result.toString();
}
/**
* Prints the queued (pending) locale definitions to a {@link PrintWriter}
* in a (UIDL) format that can be sent to the client and used there in
* formatting dates, times etc.
*
* @param outWriter
*/
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)) {
getLogger().log(Level.WARNING,
"Unable to get default date pattern for locale {0}", l);
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;
}
// Hebrew locale has time before the date
final boolean timeFirst = timeStart == 0;
String dateformat;
if (timeFirst) {
int dateStart = df.indexOf(' ');
if (ampm_first > dateStart) {
dateStart = df.indexOf(' ', ampm_first);
}
dateformat = df.substring(dateStart + 1);
} else {
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
}
protected void closeJsonMessage(PrintWriter outWriter) {
outWriter.print("}]");
}
/**
* Writes the opening of JSON message to be sent to client.
*
* @param outWriter
* @param response
*/
protected void openJsonMessage(PrintWriter outWriter,
VaadinResponse response) {
// Sets the response type
response.setContentType("application/json; charset=UTF-8");
// some dirt to prevent cross site scripting
outWriter.print("for(;;);[{");
}
/**
* Returns dirty components which are in given window. Components in an
* invisible subtrees are omitted.
*
* @param w
* UI window for which dirty components is to be fetched
* @return
*/
private ArrayList<ClientConnector> getDirtyVisibleConnectors(
ConnectorTracker connectorTracker) {
ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>();
for (ClientConnector c : connectorTracker.getDirtyConnectors()) {
if (isConnectorVisibleToClient(c)) {
dirtyConnectors.add(c);
}
}
return dirtyConnectors;
}
/**
* Queues a locale to be sent to the client (browser) for date and time
* entry etc. All locale specific information is derived from server-side
* {@link Locale} instances and sent to the client when needed, eliminating
* the need to use the {@link Locale} class and all the framework behind it
* on the client.
*
* @see Locale#toString()
*
* @param value
*/
public void requireLocale(String value) {
if (locales == null) {
locales = new ArrayList<String>();
locales.add(session.getLocale().toString());
pendingLocalesIndex = 0;
}
if (!locales.contains(value)) {
locales.add(value);
}
}
/**
* Constructs a {@link Locale} instance to be sent to the client based on a
* short locale description string.
*
* @see #requireLocale(String)
*
* @param value
* @return
*/
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]);
}
}
protected class InvalidUIDLSecurityKeyException extends
GeneralSecurityException {
InvalidUIDLSecurityKeyException(String message) {
super(message);
}
}
private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>();
private int nextTypeKey = 0;
private BootstrapHandler bootstrapHandler;
String getTagForType(Class<? extends ClientConnector> class1) {
Integer id = typeToKey.get(class1);
if (id == null) {
id = nextTypeKey++;
typeToKey.put(class1, id);
if (getLogger().isLoggable(Level.FINE)) {
getLogger().log(Level.FINE, "Mapping {0} to {1}",
new Object[] { class1.getName(), id });
}
}
return id.toString();
}
/**
* Helper class for terminal to keep track of data that client is expected
* to know.
*
* TODO make customlayout templates (from theme) to be cached here.
*/
class ClientCache implements Serializable {
private final Set<Object> res = new HashSet<Object>();
/**
*
* @param paintable
* @return true if the given class was added to cache
*/
boolean cache(Object object) {
return res.add(object);
}
public void clear() {
res.clear();
}
}
public String getStreamVariableTargetUrl(ClientConnector owner,
String name, StreamVariable value) {
/*
* We will use the same APP/* URI space as ApplicationResources but
* prefix url with UPLOAD
*
* eg. APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY]
*
* SECKEY is created on each paint to make URL's unpredictable (to
* prevent CSRF attacks).
*
* NAME and PID from URI forms a key to fetch StreamVariable when
* handling post
*/
String paintableId = owner.getConnectorId();
UI ui = owner.getUI();
int uiId = ui.getUIId();
String key = uiId + "/" + paintableId + "/" + name;
ConnectorTracker connectorTracker = ui.getConnectorTracker();
connectorTracker.addStreamVariable(paintableId, name, value);
String seckey = connectorTracker.getSeckey(value);
return ApplicationConstants.APP_PROTOCOL_PREFIX
+ ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey;
}
public void cleanStreamVariable(ClientConnector owner, String name) {
owner.getUI().getConnectorTracker()
.cleanStreamVariable(owner.getConnectorId(), name);
}
/**
* Gets the bootstrap handler that should be used for generating the pages
* bootstrapping applications for this communication manager.
*
* @return the bootstrap handler to use
*/
private BootstrapHandler getBootstrapHandler() {
if (bootstrapHandler == null) {
bootstrapHandler = createBootstrapHandler();
}
return bootstrapHandler;
}
/**
* @return
*
* @deprecated As of 7.0. Will likely change or be removed in a future
* version
*/
@Deprecated
protected abstract BootstrapHandler createBootstrapHandler();
/**
* Handles a request by passing it to each registered {@link RequestHandler}
* in turn until one produces a response. This method is used for requests
* that have not been handled by any specific functionality in the terminal
* implementation (e.g. {@link VaadinServlet}).
* <p>
* The request handlers are invoked in the revere order in which they were
* added to the session until a response has been produced. This means that
* the most recently added handler is used first and the first request
* handler that was added to the session is invoked towards the end unless
* any previous handler has already produced a response.
* </p>
*
* @param request
* the Vaadin request to get information from
* @param response
* the response to which data can be written
* @return returns <code>true</code> if a {@link RequestHandler} has
* produced a response and <code>false</code> if no response has
* been written.
* @throws IOException
* if a handler throws an exception
*
* @see VaadinSession#addRequestHandler(RequestHandler)
* @see RequestHandler
*
* @since 7.0
*/
protected boolean handleOtherRequest(VaadinRequest request,
VaadinResponse response) throws IOException {
// Use a copy to avoid ConcurrentModificationException
for (RequestHandler handler : new ArrayList<RequestHandler>(
session.getRequestHandlers())) {
if (handler.handleRequest(session, request, response)) {
return true;
}
}
// If not handled
return false;
}
public void handleBrowserDetailsRequest(VaadinRequest request,
VaadinResponse response, VaadinSession session) throws IOException {
session.lock();
try {
assert UI.getCurrent() == null;
response.setContentType("application/json; charset=UTF-8");
UI uI = getBrowserDetailsUI(request, session);
JSONObject params = new JSONObject();
params.put(UIConstants.UI_ID_PARAMETER, uI.getUIId());
String initialUIDL = getInitialUIDL(request, uI);
params.put("uidl", initialUIDL);
// NOTE! GateIn requires, for some weird reason, getOutputStream
// to be used instead of getWriter() (it seems to interpret
// application/json as a binary content type)
final OutputStream out = response.getOutputStream();
final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(out, "UTF-8")));
outWriter.write(params.toString());
// NOTE GateIn requires the buffers to be flushed to work
outWriter.flush();
out.flush();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
session.unlock();
}
}
private UI getBrowserDetailsUI(VaadinRequest request, VaadinSession session) {
VaadinService vaadinService = request.getService();
List<UIProvider> uiProviders = session.getUIProviders();
UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent(
request);
UIProvider provider = null;
Class<? extends UI> uiClass = null;
for (UIProvider p : uiProviders) {
// Check for existing LegacyWindow
if (p instanceof LegacyApplicationUIProvider) {
LegacyApplicationUIProvider legacyProvider = (LegacyApplicationUIProvider) p;
UI existingUi = legacyProvider
.getExistingUI(classSelectionEvent);
if (existingUi != null) {
reinitUI(existingUi, request);
return existingUi;
}
}
uiClass = p.getUIClass(classSelectionEvent);
if (uiClass != null) {
provider = p;
break;
}
}
if (provider == null || uiClass == null) {
return null;
}
// Check for an existing UI based on window.name
// Special parameter sent by vaadinBootstrap.js
String windowName = request.getParameter("v-wn");
Map<String, Integer> retainOnRefreshUIs = session
.getPreserveOnRefreshUIs();
if (windowName != null && !retainOnRefreshUIs.isEmpty()) {
// Check for a known UI
Integer retainedUIId = retainOnRefreshUIs.get(windowName);
if (retainedUIId != null) {
UI retainedUI = session.getUIById(retainedUIId.intValue());
if (uiClass.isInstance(retainedUI)) {
reinitUI(retainedUI, request);
return retainedUI;
} else {
getLogger().log(
Level.INFO,
"Not using retained UI in {0} because retained UI was of type {1}"
+ " but {2} is expected for the request.",
new Object[] { windowName, retainedUI.getClass(),
uiClass });
}
}
}
// No existing UI found - go on by creating and initializing one
Integer uiId = Integer.valueOf(session.getNextUIid());
// Explicit Class.cast to detect if the UIProvider does something
// unexpected
UICreateEvent event = new UICreateEvent(request, uiClass, uiId);
UI ui = uiClass.cast(provider.createInstance(event));
// Initialize some fields for a newly created UI
if (ui.getSession() != session) {
// Session already set for LegacyWindow
ui.setSession(session);
}
// Set thread local here so it is available in init
UI.setCurrent(ui);
ui.doInit(request, uiId.intValue());
session.addUI(ui);
// Remember if it should be remembered
if (vaadinService.preserveUIOnRefresh(provider, event)) {
// Remember this UI
if (windowName == null) {
getLogger()
.log(Level.WARNING,
"There is no window.name available for UI {0} that should be preserved.",
uiClass);
} else {
session.getPreserveOnRefreshUIs().put(windowName, uiId);
}
}
return ui;
}
/**
* Updates a UI that has already been initialized but is now loaded again,
* e.g. because of {@link PreserveOnRefresh}.
*
* @param ui
* @param request
*/
private void reinitUI(UI ui, VaadinRequest request) {
UI.setCurrent(ui);
// Fire fragment change if the fragment has changed
String location = request.getParameter("v-loc");
if (location != null) {
ui.getPage().updateLocation(location);
}
}
/**
* Generates the initial UIDL message that can e.g. be included in a html
* page to avoid a separate round trip just for getting the UIDL.
*
* @param request
* the request that caused the initialization
* @param uI
* the UI for which the UIDL should be generated
* @return a string with the initial UIDL message
* @throws PaintException
* if an exception occurs while painting
* @throws JSONException
* if an exception occurs while encoding output
*/
protected String getInitialUIDL(VaadinRequest request, UI uI)
throws PaintException, JSONException {
// TODO maybe unify writeUidlResponse()?
StringWriter sWriter = new StringWriter();
PrintWriter pWriter = new PrintWriter(sWriter);
pWriter.print("{");
if (isXSRFEnabled(uI.getSession())) {
pWriter.print(getSecurityKeyUIDL(request));
}
writeUidlResponse(request, true, pWriter, uI, false);
pWriter.print("}");
String initialUIDL = sWriter.toString();
getLogger().log(Level.FINE, "Initial UIDL:{0}", initialUIDL);
return initialUIDL;
}
/**
* Serve a connector resource from the classpath if the resource has
* previously been registered by calling
* {@link #registerPublishedFile(String, Class)}. Sending arbitrary files
* from the classpath is prevented by only accepting resource names that
* have explicitly been registered. Resources can currently only be
* registered by including a {@link JavaScript} or {@link StyleSheet}
* annotation on a Connector class.
*
* @param request
* @param response
*
* @throws IOException
*/
public void servePublishedFile(VaadinRequest request,
VaadinResponse response) throws IOException {
String pathInfo = request.getPathInfo();
// + 2 to also remove beginning and ending slashes
String fileName = pathInfo
.substring(ApplicationConstants.PUBLISHED_FILE_PATH.length() + 2);
final String mimetype = response.getService().getMimeType(fileName);
// Security check: avoid accidentally serving from the UI of the
// classpath instead of relative to the context class
if (fileName.startsWith("/")) {
getLogger().log(Level.WARNING,
"Published file request starting with / rejected: {0}",
fileName);
response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName);
return;
}
// Check that the resource name has been registered
Class<?> context;
synchronized (publishedFileContexts) {
context = publishedFileContexts.get(fileName);
}
// Security check: don't serve resource if the name hasn't been
// registered in the map
if (context == null) {
getLogger()
.log(Level.WARNING,
"Rejecting published file request for file that has not been published: {0}",
fileName);
response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName);
return;
}
// Resolve file relative to the location of the context class
InputStream in = context.getResourceAsStream(fileName);
if (in == null) {
getLogger()
.log(Level.WARNING,
"{0} published by {1} not found. Verify that the file {2}/{3} is available on the classpath.",
new Object[] {
fileName,
context.getName(),
context.getPackage().getName()
.replace('.', '/'), fileName });
response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName);
return;
}
// TODO Check and set cache headers
OutputStream out = null;
try {
if (mimetype != null) {
response.setContentType(mimetype);
}
out = response.getOutputStream();
final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE];
int bytesRead = 0;
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
out.flush();
} finally {
try {
in.close();
} catch (Exception e) {
// Do nothing
}
if (out != null) {
try {
out.close();
} catch (Exception e) {
// Do nothing
}
}
}
}
/**
* Handles file upload request submitted via Upload component.
*
* @param UI
* The UI for this request
*
* @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable)
*
* @param request
* @param response
* @throws IOException
* @throws InvalidUIDLSecurityKeyException
*/
public void handleFileUpload(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException,
InvalidUIDLSecurityKeyException {
/*
* URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See
* #createReceiverUrl
*/
String pathInfo = request.getPathInfo();
// strip away part until the data we are interested starts
int startOfData = pathInfo
.indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX)
+ ServletPortletHelper.UPLOAD_URL_PREFIX.length();
String uppUri = pathInfo.substring(startOfData);
String[] parts = uppUri.split("/", 4); // 0= UIid, 1 = cid, 2= name, 3
// = sec key
String uiId = parts[0];
String connectorId = parts[1];
String variableName = parts[2];
UI uI = session.getUIById(Integer.parseInt(uiId));
UI.setCurrent(uI);
StreamVariable streamVariable = uI.getConnectorTracker()
.getStreamVariable(connectorId, variableName);
String secKey = uI.getConnectorTracker().getSeckey(streamVariable);
if (secKey.equals(parts[3])) {
ClientConnector source = getConnector(uI, connectorId);
String contentType = request.getContentType();
if (contentType.contains("boundary")) {
// Multipart requests contain boundary string
doHandleSimpleMultipartFileUpload(request, response,
streamVariable, variableName, source,
contentType.split("boundary=")[1]);
} else {
// if boundary string does not exist, the posted file is from
// XHR2.post(File)
doHandleXhrFilePost(request, response, streamVariable,
variableName, source, request.getContentLength());
}
} else {
throw new InvalidUIDLSecurityKeyException(
"Security key in upload post did not match!");
}
}
/**
* Handles a heartbeat request. Heartbeat requests are periodically sent by
* the client-side to inform the server that the UI sending the heartbeat is
* still alive (the browser window is open, the connection is up) even when
* there are no UIDL requests for a prolonged period of time. UIs that do
* not receive either heartbeat or UIDL requests are eventually removed from
* the session and garbage collected.
*
* @param request
* @param response
* @param session
* @throws IOException
*/
public void handleHeartbeatRequest(VaadinRequest request,
VaadinResponse response, VaadinSession session) throws IOException {
UI ui = null;
try {
int uiId = Integer.parseInt(request
.getParameter(UIConstants.UI_ID_PARAMETER));
ui = session.getUIById(uiId);
} catch (NumberFormatException nfe) {
// null-check below handles this as well
}
if (ui != null) {
ui.setLastHeartbeatTimestamp(System.currentTimeMillis());
// Ensure that the browser does not cache heartbeat responses.
// iOS 6 Safari requires this (#10370)
response.setHeader("Cache-Control", "no-cache");
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found");
}
}
/**
* Stream that extracts content from another stream until the boundary
* string is encountered.
*
* Public only for unit tests, should be considered private for all other
* purposes.
*/
public static class SimpleMultiPartInputStream extends InputStream {
/**
* Counter of how many characters have been matched to boundary string
* from the stream
*/
int matchedCount = -1;
/**
* Used as pointer when returning bytes after partly matched boundary
* string.
*/
int curBoundaryIndex = 0;
/**
* The byte found after a "promising start for boundary"
*/
private int bufferedByte = -1;
private boolean atTheEnd = false;
private final char[] boundary;
private final InputStream realInputStream;
public SimpleMultiPartInputStream(InputStream realInputStream,
String boundaryString) {
boundary = (CRLF + DASHDASH + boundaryString).toCharArray();
this.realInputStream = realInputStream;
}
@Override
public int read() throws IOException {
if (atTheEnd) {
// End boundary reached, nothing more to read
return -1;
} else if (bufferedByte >= 0) {
/* Purge partially matched boundary if there was such */
return getBuffered();
} else if (matchedCount != -1) {
/*
* Special case where last "failed" matching ended with first
* character from boundary string
*/
return matchForBoundary();
} else {
int fromActualStream = realInputStream.read();
if (fromActualStream == -1) {
// unexpected end of stream
throw new IOException(
"The multipart stream ended unexpectedly");
}
if (boundary[0] == fromActualStream) {
/*
* If matches the first character in boundary string, start
* checking if the boundary is fetched.
*/
return matchForBoundary();
}
return fromActualStream;
}
}
/**
* Reads the input to expect a boundary string. Expects that the first
* character has already been matched.
*
* @return -1 if the boundary was matched, else returns the first byte
* from boundary
* @throws IOException
*/
private int matchForBoundary() throws IOException {
matchedCount = 0;
/*
* Going to "buffered mode". Read until full boundary match or a
* different character.
*/
while (true) {
matchedCount++;
if (matchedCount == boundary.length) {
/*
* The whole boundary matched so we have reached the end of
* file
*/
atTheEnd = true;
return -1;
}
int fromActualStream = realInputStream.read();
if (fromActualStream != boundary[matchedCount]) {
/*
* Did not find full boundary, cache the mismatching byte
* and start returning the partially matched boundary.
*/
bufferedByte = fromActualStream;
return getBuffered();
}
}
}
/**
* Returns the partly matched boundary string and the byte following
* that.
*
* @return
* @throws IOException
*/
private int getBuffered() throws IOException {
int b;
if (matchedCount == 0) {
// The boundary has been returned, return the buffered byte.
b = bufferedByte;
bufferedByte = -1;
matchedCount = -1;
} else {
b = boundary[curBoundaryIndex++];
if (curBoundaryIndex == matchedCount) {
// The full boundary has been returned, remaining is the
// char that did not match the boundary.
curBoundaryIndex = 0;
if (bufferedByte != boundary[0]) {
/*
* next call for getBuffered will return the
* bufferedByte that came after the partial boundary
* match
*/
matchedCount = 0;
} else {
/*
* Special case where buffered byte again matches the
* boundaryString. This could be the start of the real
* end boundary.
*/
matchedCount = 0;
bufferedByte = -1;
}
}
}
if (b == -1) {
throw new IOException("The multipart stream ended unexpectedly");
}
return b;
}
}
private static final Logger getLogger() {
return Logger.getLogger(AbstractCommunicationManager.class.getName());
}
}