svn changeset:15461/svn branch:6.5tags/6.7.0.beta1
@@ -180,7 +180,6 @@ | |||
<!-- Construct classpath used by java and javadoc compilation --> | |||
<path id="compile.classpath"> | |||
<pathelement path="build/lib/servlet.jar" /> | |||
<pathelement path="build/external/fileupload/classes" /> | |||
<fileset dir="lib/core"> | |||
<include name="**/*.jar"/> | |||
@@ -574,12 +573,7 @@ | |||
</copy> | |||
</target> | |||
<target name="compile-fileupload"> | |||
<echo>Compiling custom fileupload classes.</echo> | |||
<ant dir="build/external/fileupload" antfile="build.xml" target="compile" /> | |||
</target> | |||
<target name="compile-java" depends="init, check-servlet-version, compile-fileupload, webcontent"> | |||
<target name="compile-java" depends="init, check-servlet-version, webcontent"> | |||
<echo>Compiling src (server-side)</echo> | |||
<!-- Compile all sources at the same time as they depend on each other --> | |||
@@ -798,10 +792,6 @@ | |||
<exclude name="${toolkit-package}/launcher/**" /> | |||
</patternset> | |||
</fileset> | |||
<!-- fileupload, see build/external/fileupload/build.xml --> | |||
<fileset dir="build/external/fileupload/classes"> | |||
<include name="**/*" /> | |||
</fileset> | |||
<!-- add sources --> | |||
<fileset dir="${result-path}/src/core"> | |||
<patternset> |
@@ -151,6 +151,22 @@ public interface PaintTarget extends Serializable { | |||
*/ | |||
public void addAttribute(String name, Resource value) throws PaintException; | |||
/** | |||
* Adds a Receiver attribute to component. Eg. in web terminals Receivers | |||
* are typically URIs, where the client side can do an http post (multipart | |||
* request). | |||
* | |||
* @param name | |||
* the Attribute name | |||
* @param value | |||
* the Attribute value | |||
* | |||
* @throws PaintException | |||
* if the paint operation failed. | |||
*/ | |||
public void addVariable(ReceiverOwner owner, String name, Receiver value) | |||
throws PaintException; | |||
/** | |||
* Adds a long attribute to component. Atributes must be added before any | |||
* content is written. |
@@ -0,0 +1,29 @@ | |||
package com.vaadin.terminal; | |||
import java.io.OutputStream; | |||
import java.io.Serializable; | |||
/** | |||
* | |||
* Interface that must be implemented by the upload receivers to provide the | |||
* Upload component an output stream to write the uploaded data. | |||
* | |||
* @author IT Mill Ltd. | |||
* @version | |||
* @VERSION@ | |||
* @since 6.5 | |||
*/ | |||
public interface Receiver extends Serializable { | |||
/** | |||
* Invoked when a new upload arrives. | |||
* | |||
* @param filename | |||
* the desired filename of the upload, usually as specified by | |||
* the client. | |||
* @param MIMEType | |||
* the MIME type of the uploaded file. | |||
* @return Stream to which the uploaded file should be written. | |||
*/ | |||
public OutputStream receiveUpload(String filename, String MIMEType); | |||
} |
@@ -0,0 +1,161 @@ | |||
package com.vaadin.terminal; | |||
import java.io.Serializable; | |||
import com.vaadin.Application; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingController; | |||
/** | |||
* Special kind of {@link VariableOwner} that can send and receive information | |||
* with the terminal implementation about the progress of receiving data to its | |||
* Receiver. The actual communication happens via {@link ReceivingController} | |||
* which is fetched by the terminal when the Receiving is about to start. | |||
*/ | |||
public interface ReceiverOwner extends VariableOwner { | |||
/* | |||
* The monitor/control is passed to separate ReceivingContorller because: | |||
* | |||
* - possibly some component in the future may need support for streaming to | |||
* multiple Receivers at the same time. | |||
* | |||
* - we don't want to bloat implementing ReceiverOwner's API. Now only one | |||
* method is published and they can decide what event/methods to publish as | |||
* their public API. | |||
*/ | |||
/** | |||
* Returns a handle for the terminal via the ReceiverOwner can monitor and | |||
* control the steaming of data to {@link Receiver}. | |||
* <p> | |||
* Most commonly ReceiverOwner implementation wants to implement this method | |||
* as final and reveal its own API for the end users. | |||
* | |||
* @param receiver | |||
* the Receiver whose streaming is to be controlled | |||
* @return a {@link ReceivingController} that will be used to control and | |||
* monitor the progress of streaming | |||
*/ | |||
ReceivingController getReceivingController(Receiver receiver); | |||
interface ReceivingEvent extends Serializable { | |||
/** | |||
* @return the file name of the streamed file if known | |||
*/ | |||
String getFileName(); | |||
/** | |||
* @return the mime type of the streamed file if known | |||
*/ | |||
String getMimeType(); | |||
/** | |||
* @return the Receiver into which the content is being streamed | |||
*/ | |||
Receiver getReceiver(); | |||
/** | |||
* @return the length of the stream (in bytes) if known, else -1 | |||
*/ | |||
long getContentLength(); | |||
/** | |||
* @return then number of bytes streamed to Receiver | |||
*/ | |||
long getBytesReceived(); | |||
} | |||
/** | |||
* Event passed to | |||
* {@link ReceivingController#uploadStarted(ReceivingStartedEvent)} method | |||
* before the streaming of the content to {@link Receiver} starts. | |||
*/ | |||
public interface ReceivingStartedEvent extends ReceivingEvent { | |||
} | |||
/** | |||
* Event passed to | |||
* {@link ReceivingController#onProgress(ReceivingProgressedEvent)} method | |||
* during the streaming progresses. | |||
*/ | |||
public interface ReceivingProgressedEvent extends ReceivingEvent { | |||
} | |||
/** | |||
* Event passed to | |||
* {@link ReceivingController#uploadFinished(ReceivingEndedEvent)} method | |||
* the contents have been streamed to Receiver successfully. | |||
*/ | |||
public interface ReceivingEndedEvent extends ReceivingEvent { | |||
} | |||
/** | |||
* Event passed to | |||
* {@link ReceivingController#uploadFailed(ReceivingFailedEvent)} method | |||
* when the streaming ended before the end of the input. The streaming may | |||
* fail due an interruption by {@link ReceivingController} or due an other | |||
* unknown exception in communication. In the latter case the exception is | |||
* also passed to | |||
* {@link Application#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)} | |||
* . | |||
*/ | |||
public interface ReceivingFailedEvent extends ReceivingEvent { | |||
/** | |||
* @return the exception that caused the receiving not to finish cleanly | |||
*/ | |||
Exception getException(); | |||
} | |||
public interface ReceivingController { | |||
/** | |||
* Whether the {@link #onProgress(long, long)} method should be called | |||
* during the upload. | |||
* <p> | |||
* {@link #onProgress(long, long)} is called in a synchronized block | |||
* during the content is being received. This is potentially bit slow, | |||
* so we are calling this method only if requested. The value is | |||
* requested after the {@link #uploadStarted(ReceivingStartedEvent)} | |||
* event, but not after each buffer reading. | |||
* | |||
* @return true if this ReceiverOwner wants to by notified during the | |||
* upload of the progress of streaming. | |||
* @see ReceiverOwner#onProgress(int, int) | |||
*/ | |||
boolean listenProgress(); | |||
/** | |||
* This method is called by the terminal if {@link #listenProgress()} | |||
* returns true when the streaming starts. | |||
*/ | |||
void onProgress(ReceivingProgressedEvent event); | |||
void uploadStarted(ReceivingStartedEvent event); | |||
void uploadFinished(ReceivingEndedEvent event); | |||
void uploadFailed(ReceivingFailedEvent event); | |||
/* | |||
* Not synchronized to avoid stalls (caused by UIDL requests) while | |||
* streaming the content. Implementations also most commonly atomic even | |||
* without the restriction. | |||
*/ | |||
/** | |||
* ReceiverOwner can set this flag to true if it wants the Terminal to | |||
* stop receiving current upload. | |||
* <p> | |||
* Note, the usage of this method is not synchronized over the | |||
* Application instance by the terminal like other methods. The | |||
* implementation should only return a boolean field and especially not | |||
* to modify UI or implement a synchronization by itself. | |||
* | |||
* @return true if the streaming should be interrupted as soon as | |||
* possible. | |||
*/ | |||
boolean isInterrupted(); | |||
} | |||
} |
@@ -3,8 +3,13 @@ | |||
*/ | |||
package com.vaadin.terminal.gwt.client.ui; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import com.google.gwt.core.client.GWT; | |||
import com.google.gwt.core.client.JavaScriptObject; | |||
import com.google.gwt.core.client.JsArrayString; | |||
import com.google.gwt.event.dom.client.MouseDownEvent; | |||
@@ -33,7 +38,6 @@ import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File.Callback; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; | |||
@@ -97,45 +101,115 @@ public class VDragAndDropWrapper extends VCustomComponent implements | |||
private int dragStarMode; | |||
private int filecounter = 0; | |||
private boolean dragLeavPostponed; | |||
private Map<String, String> fileIdToReveiver; | |||
@Override | |||
public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { | |||
this.client = client; | |||
super.updateFromUIDL(uidl, client); | |||
if (!uidl.hasAttribute("cached") && !uidl.hasAttribute("hidden")) { | |||
int childCount = uidl.getChildCount(); | |||
if (childCount > 1) { | |||
UIDL childUIDL = uidl.getChildUIDL(1); | |||
UIDL acceptCrit = uidl.getChildByTagName("-ac"); | |||
if (acceptCrit == null) { | |||
dropHandler = null; | |||
} else { | |||
if (dropHandler == null) { | |||
dropHandler = new CustomDropHandler(); | |||
} | |||
dropHandler.updateAcceptRules(childUIDL); | |||
} else { | |||
dropHandler = null; | |||
dropHandler.updateAcceptRules(acceptCrit); | |||
} | |||
Set<String> variableNames = uidl.getVariableNames(); | |||
for (String fileId : variableNames) { | |||
if (fileId.startsWith("rec-")) { | |||
String receiverUrl = uidl.getStringVariable(fileId); | |||
fileId = fileId.substring(4); | |||
if (fileIdToReveiver == null) { | |||
fileIdToReveiver = new HashMap<String, String>(); | |||
} | |||
if ("".equals(receiverUrl)) { | |||
Integer id = Integer.parseInt(fileId); | |||
int indexOf = fileIds.indexOf(id); | |||
if (indexOf != -1) { | |||
files.remove(indexOf); | |||
fileIds.remove(indexOf); | |||
} | |||
} else { | |||
fileIdToReveiver.put(fileId, receiverUrl); | |||
} | |||
} | |||
} | |||
startNextUpload(); | |||
dragStarMode = uidl.getIntAttribute("dragStartMode"); | |||
} | |||
} | |||
private boolean uploading; | |||
private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() { | |||
public void onReadyStateChange(XMLHttpRequest xhr) { | |||
if (xhr.getReadyState() == XMLHttpRequest.DONE) { | |||
// visit server for possible | |||
// variable changes | |||
client.sendPendingVariableChanges(); | |||
uploading = false; | |||
startNextUpload(); | |||
xhr.clearOnReadyStateChange(); | |||
} | |||
} | |||
}; | |||
private void startNextUpload() { | |||
DeferredCommand.addCommand(new Command() { | |||
public void execute() { | |||
if (!uploading) { | |||
if (fileIds.size() > 0) { | |||
uploading = true; | |||
final Integer fileId = fileIds.remove(0); | |||
VHtml5File file = files.remove(0); | |||
final String receiverUrl = fileIdToReveiver | |||
.remove(fileId.toString()); | |||
ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR | |||
.create(); | |||
extendedXHR | |||
.setOnReadyStateChange(readyStateChangeHandler); | |||
extendedXHR.open("POST", receiverUrl); | |||
extendedXHR.postFile(file); | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
public boolean html5DragEnter(VHtml5DragEvent event) { | |||
if (dropHandler == null) { | |||
return true; | |||
} | |||
if (dragLeavPostponed) { | |||
// returned quickly back to wrapper | |||
dragLeavPostponed = false; | |||
try { | |||
if (dragLeavPostponed) { | |||
// returned quickly back to wrapper | |||
dragLeavPostponed = false; | |||
return false; | |||
} | |||
VTransferable transferable = new VTransferable(); | |||
transferable.setDragSource(this); | |||
vaadinDragEvent = VDragAndDropManager.get().startDrag(transferable, | |||
event, false); | |||
VDragAndDropManager.get().setCurrentDropHandler(getDropHandler()); | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
return false; | |||
} catch (Exception e) { | |||
GWT.getUncaughtExceptionHandler().onUncaughtException(e); | |||
return true; | |||
} | |||
VTransferable transferable = new VTransferable(); | |||
transferable.setDragSource(this); | |||
vaadinDragEvent = VDragAndDropManager.get().startDrag(transferable, | |||
event, false); | |||
VDragAndDropManager.get().setCurrentDropHandler(getDropHandler()); | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
return false; | |||
} | |||
public boolean html5DragLeave(VHtml5DragEvent event) { | |||
@@ -143,24 +217,32 @@ public class VDragAndDropWrapper extends VCustomComponent implements | |||
return true; | |||
} | |||
dragLeavPostponed = true; | |||
DeferredCommand.addCommand(new Command() { | |||
public void execute() { | |||
// Yes, dragleave happens before drop. Makes no sense to me. | |||
// IMO shouldn't fire leave at all if drop happens (I guess this | |||
// is what IE does). | |||
// In Vaadin we fire it only if drop did not happen. | |||
if (dragLeavPostponed | |||
&& vaadinDragEvent != null | |||
&& VDragAndDropManager.get().getCurrentDropHandler() == getDropHandler()) { | |||
VDragAndDropManager.get().interruptDrag(); | |||
try { | |||
dragLeavPostponed = true; | |||
DeferredCommand.addCommand(new Command() { | |||
public void execute() { | |||
// Yes, dragleave happens before drop. Makes no sense to me. | |||
// IMO shouldn't fire leave at all if drop happens (I guess | |||
// this | |||
// is what IE does). | |||
// In Vaadin we fire it only if drop did not happen. | |||
if (dragLeavPostponed | |||
&& vaadinDragEvent != null | |||
&& VDragAndDropManager.get() | |||
.getCurrentDropHandler() == getDropHandler()) { | |||
VDragAndDropManager.get().interruptDrag(); | |||
} | |||
dragLeavPostponed = false; | |||
} | |||
dragLeavPostponed = false; | |||
} | |||
}); | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
return false; | |||
}); | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
return false; | |||
} catch (Exception e) { | |||
GWT.getUncaughtExceptionHandler().onUncaughtException(e); | |||
return true; | |||
} | |||
} | |||
public boolean html5DragOver(VHtml5DragEvent event) { | |||
@@ -188,41 +270,47 @@ public class VDragAndDropWrapper extends VCustomComponent implements | |||
if (dropHandler == null || !currentlyValid) { | |||
return true; | |||
} | |||
try { | |||
VTransferable transferable = vaadinDragEvent.getTransferable(); | |||
VTransferable transferable = vaadinDragEvent.getTransferable(); | |||
JsArrayString types = event.getTypes(); | |||
for (int i = 0; i < types.length(); i++) { | |||
String type = types.get(i); | |||
if (isAcceptedType(type)) { | |||
String data = event.getDataAsText(type); | |||
if (data != null) { | |||
transferable.setData(type, data); | |||
JsArrayString types = event.getTypes(); | |||
for (int i = 0; i < types.length(); i++) { | |||
String type = types.get(i); | |||
if (isAcceptedType(type)) { | |||
String data = event.getDataAsText(type); | |||
if (data != null) { | |||
transferable.setData(type, data); | |||
} | |||
} | |||
} | |||
} | |||
int fileCount = event.getFileCount(); | |||
if (fileCount > 0) { | |||
transferable.setData("filecount", fileCount); | |||
for (int i = 0; i < fileCount; i++) { | |||
final int fileId = filecounter++; | |||
final VHtml5File file = event.getFile(i); | |||
transferable.setData("fi" + i, "" + fileId); | |||
transferable.setData("fn" + i, file.getName()); | |||
transferable.setData("ft" + i, file.getType()); | |||
transferable.setData("fs" + i, file.getSize()); | |||
postFile(fileId, file); | |||
int fileCount = event.getFileCount(); | |||
if (fileCount > 0) { | |||
transferable.setData("filecount", fileCount); | |||
for (int i = 0; i < fileCount; i++) { | |||
final int fileId = filecounter++; | |||
final VHtml5File file = event.getFile(i); | |||
transferable.setData("fi" + i, "" + fileId); | |||
transferable.setData("fn" + i, file.getName()); | |||
transferable.setData("ft" + i, file.getType()); | |||
transferable.setData("fs" + i, file.getSize()); | |||
queueFilePost(fileId, file); | |||
} | |||
} | |||
} | |||
VDragAndDropManager.get().endDrag(); | |||
vaadinDragEvent = null; | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
VDragAndDropManager.get().endDrag(); | |||
vaadinDragEvent = null; | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
return false; | |||
} catch (Exception e) { | |||
GWT.getUncaughtExceptionHandler().onUncaughtException(e); | |||
return true; | |||
} | |||
return false; | |||
} | |||
protected String[] acceptedTypes = new String[] { "Text", "Url", | |||
@@ -242,10 +330,14 @@ public class VDragAndDropWrapper extends VCustomComponent implements | |||
protected ExtendedXHR() { | |||
} | |||
public final native void sendBinary(JavaScriptObject data) | |||
public final native void postFile(VHtml5File file) | |||
/*-{ | |||
//this.overrideMimeType('text/plain; charset=x-user-defined-binary'); | |||
this.sendAsBinary(data); | |||
// Accept header is readable in portlets resourceRequest | |||
// TODO add filename and mime type too?? | |||
this.setRequestHeader('Accept', 'text/html,vaadin/filexhr'); | |||
this.setRequestHeader('Content-Type', 'multipart/form-data'); | |||
this.send(file); | |||
}-*/; | |||
} | |||
@@ -257,38 +349,12 @@ public class VDragAndDropWrapper extends VCustomComponent implements | |||
* @param fileId | |||
* @param data | |||
*/ | |||
private void postFile(final int fileId, final VHtml5File file) { | |||
DeferredCommand.addCommand(new Command() { | |||
public void execute() { | |||
/* | |||
* File contents is sent deferred to allow quick reaction on GUI | |||
* although file upload may last long. | |||
*/ | |||
file.readAsBinary(new Callback() { | |||
public void handleFile(final JavaScriptObject object) { | |||
ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR | |||
.create(); | |||
String name = "XHRFILE" + getPid() + "." + fileId; | |||
extendedXHR | |||
.setOnReadyStateChange(new ReadyStateChangeHandler() { | |||
public void onReadyStateChange( | |||
XMLHttpRequest xhr) { | |||
if (xhr.getReadyState() == XMLHttpRequest.DONE) { | |||
client.sendPendingVariableChanges(); | |||
xhr.clearOnReadyStateChange(); | |||
} | |||
} | |||
}); | |||
extendedXHR.open("POST", client.getAppUri()); | |||
multipartSend(extendedXHR, object, name); | |||
} | |||
}); | |||
} | |||
}); | |||
private List<Integer> fileIds = new ArrayList<Integer>(); | |||
private List<VHtml5File> files = new ArrayList<VHtml5File>(); | |||
private void queueFilePost(final int fileId, final VHtml5File file) { | |||
fileIds.add(fileId); | |||
files.add(file); | |||
} | |||
private String getPid() { |
@@ -147,7 +147,7 @@ public class VUpload extends SimplePanel implements Paintable { | |||
this.client = client; | |||
paintableId = uidl.getId(); | |||
nextUploadId = uidl.getIntAttribute("nextid"); | |||
element.setAction(client.getAppUri()); | |||
element.setAction(uidl.getStringVariable("action")); | |||
submitButton.setText(uidl.getStringAttribute("buttoncaption")); | |||
fu.setName(paintableId + "_file"); | |||
@@ -49,7 +49,6 @@ import com.liferay.portal.kernel.util.PortalClassInvoker; | |||
import com.liferay.portal.kernel.util.PropsUtil; | |||
import com.vaadin.Application; | |||
import com.vaadin.Application.SystemMessages; | |||
import com.vaadin.external.org.apache.commons.fileupload.portlet.PortletFileUpload; | |||
import com.vaadin.terminal.DownloadStream; | |||
import com.vaadin.terminal.Terminal; | |||
import com.vaadin.terminal.gwt.client.ApplicationConfiguration; | |||
@@ -259,6 +258,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
} else if (request instanceof ResourceRequest) { | |||
if (isUIDLRequest((ResourceRequest) request)) { | |||
return RequestType.UIDL; | |||
} else if (isFileUploadRequest((ResourceRequest) request)) { | |||
return RequestType.FILE_UPLOAD; | |||
} else if (isApplicationResourceRequest((ResourceRequest) request)) { | |||
return RequestType.APPLICATION_RESOURCE; | |||
} else if (isDummyRequest((ResourceRequest) request)) { | |||
@@ -267,12 +268,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
return RequestType.STATIC_FILE; | |||
} | |||
} else if (request instanceof ActionRequest) { | |||
if (isFileUploadRequest((ActionRequest) request)) { | |||
return RequestType.FILE_UPLOAD; | |||
} else { | |||
// action other than upload | |||
return RequestType.ACTION; | |||
} | |||
return RequestType.ACTION; | |||
} else if (request instanceof EventRequest) { | |||
return RequestType.EVENT; | |||
} | |||
@@ -294,8 +290,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
&& request.getResourceID().equals("DUMMY"); | |||
} | |||
private boolean isFileUploadRequest(ActionRequest request) { | |||
return PortletFileUpload.isMultipartContent(request); | |||
private boolean isFileUploadRequest(ResourceRequest request) { | |||
return "UPLOAD".equals(request.getResourceID()); | |||
} | |||
/** | |||
@@ -356,19 +352,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
PortletCommunicationManager applicationManager = applicationContext | |||
.getApplicationManager(application); | |||
if (response instanceof RenderResponse | |||
&& applicationManager.dummyURL == null) { | |||
/* | |||
* The application manager needs an URL to the dummy page. | |||
* See the PortletCommunicationManager.sendUploadResponse | |||
* method for more information. | |||
*/ | |||
ResourceURL dummyURL = ((RenderResponse) response) | |||
.createResourceURL(); | |||
dummyURL.setResourceID("DUMMY"); | |||
applicationManager.dummyURL = dummyURL.toString(); | |||
} | |||
/* Update browser information from request */ | |||
updateBrowserProperties(applicationContext.getBrowser(), | |||
request); | |||
@@ -438,7 +421,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
/* Handle the request */ | |||
if (requestType == RequestType.FILE_UPLOAD) { | |||
applicationManager.handleFileUpload( | |||
(ActionRequest) request, (ActionResponse) response); | |||
(ResourceRequest) request, | |||
(ResourceResponse) response); | |||
return; | |||
} else if (requestType == RequestType.UIDL) { | |||
// Handles AJAX UIDL requests | |||
@@ -1054,7 +1038,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet | |||
Map<String, String> config = new LinkedHashMap<String, String>(); | |||
/* | |||
* We need this in order to get uploads to work. | |||
* We need this in order to get uploads to work. TODO this is not needed | |||
* for uploads anymore, check if this is needed for some other things | |||
*/ | |||
PortletURL appUri = response.createActionURL(); | |||
config.put("appUri", "'" + appUri.toString() + "'"); |
@@ -34,7 +34,6 @@ import javax.servlet.http.HttpSession; | |||
import com.vaadin.Application; | |||
import com.vaadin.Application.SystemMessages; | |||
import com.vaadin.external.org.apache.commons.fileupload.servlet.ServletFileUpload; | |||
import com.vaadin.terminal.DownloadStream; | |||
import com.vaadin.terminal.ParameterHandler; | |||
import com.vaadin.terminal.Terminal; | |||
@@ -177,6 +176,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements | |||
private final String resourcePath = null; | |||
private int resourceCacheTime = 3600; | |||
static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; | |||
/** | |||
* Called by the servlet container to indicate to a servlet that the servlet | |||
@@ -1362,7 +1362,18 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements | |||
} | |||
private boolean isFileUploadRequest(HttpServletRequest request) { | |||
return ServletFileUpload.isMultipartContent(request); | |||
String pathInfo = getRequestPathInfo(request); | |||
if (pathInfo == null) { | |||
return false; | |||
} | |||
if (pathInfo.startsWith("/" + UPLOAD_URL_PREFIX)) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
private boolean isOnUnloadRequest(HttpServletRequest request) { |
@@ -43,29 +43,26 @@ import javax.servlet.ServletResponse; | |||
import com.vaadin.Application; | |||
import com.vaadin.Application.SystemMessages; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileItemStream; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUpload; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; | |||
import com.vaadin.external.org.apache.commons.fileupload.ProgressListener; | |||
import com.vaadin.terminal.ApplicationResource; | |||
import com.vaadin.terminal.DownloadStream; | |||
import com.vaadin.terminal.PaintException; | |||
import com.vaadin.terminal.PaintTarget; | |||
import com.vaadin.terminal.Paintable; | |||
import com.vaadin.terminal.Paintable.RepaintRequestEvent; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingController; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; | |||
import com.vaadin.terminal.Terminal.ErrorEvent; | |||
import com.vaadin.terminal.Terminal.ErrorListener; | |||
import com.vaadin.terminal.URIHandler; | |||
import com.vaadin.terminal.UploadStream; | |||
import com.vaadin.terminal.VariableOwner; | |||
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.DragAndDropWrapper; | |||
import com.vaadin.ui.Upload; | |||
import com.vaadin.ui.Upload.UploadException; | |||
import com.vaadin.ui.Window; | |||
/** | |||
@@ -261,6 +258,12 @@ public abstract class AbstractCommunicationManager implements | |||
} | |||
static class UploadInterruptedException extends Exception { | |||
public UploadInterruptedException() { | |||
super("Upload interrupted by other thread"); | |||
} | |||
} | |||
private static String GET_PARAM_REPAINT_ALL = "repaintAll"; | |||
// flag used in the request to indicate that the security token should be | |||
@@ -296,6 +299,9 @@ public abstract class AbstractCommunicationManager implements | |||
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; | |||
private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; | |||
private final ArrayList<Paintable> dirtyPaintables = new ArrayList<Paintable>(); | |||
@@ -332,146 +338,319 @@ public abstract class AbstractCommunicationManager implements | |||
requireLocale(application.getLocale().toString()); | |||
} | |||
/** | |||
* Create an upload handler that is appropriate to the context in which the | |||
* application is being run (servlet or portlet). | |||
* | |||
* @return new {@link FileUpload} instance | |||
*/ | |||
protected abstract FileUpload createFileUpload(); | |||
protected Application getApplication() { | |||
return application; | |||
} | |||
/** | |||
* TODO New method - document me! | |||
* | |||
* @param upload | |||
* @param request | |||
* @return | |||
* @throws IOException | |||
* @throws FileUploadException | |||
*/ | |||
protected abstract FileItemIterator getUploadItemIterator( | |||
FileUpload upload, Request request) throws IOException, | |||
FileUploadException; | |||
private static final int LF = "\n".getBytes()[0]; | |||
private static final String CRLF = "\r\n"; | |||
private static String readLine(InputStream stream) throws IOException { | |||
StringBuilder sb = new StringBuilder(); | |||
int readByte = stream.read(); | |||
while (readByte != LF) { | |||
char c = (char) readByte; | |||
sb.append(c); | |||
readByte = stream.read(); | |||
} | |||
return sb.substring(0, sb.length() - 1); | |||
} | |||
/** | |||
* TODO New method - document me! | |||
* Method used to stream content from a multipart request (either from | |||
* servlet or portlet request) to given Receiver | |||
* | |||
* | |||
* @param request | |||
* @param response | |||
* @param receiver | |||
* @param owner | |||
* @param boundary | |||
* @throws IOException | |||
* @throws FileUploadException | |||
*/ | |||
protected void doHandleFileUpload(Request request, Response response) | |||
throws IOException, FileUploadException { | |||
protected void doHandleSimpleMultipartFileUpload(Request request, | |||
Response response, Receiver receiver, ReceiverOwner owner, | |||
String boundary) throws IOException { | |||
boundary = CRLF + "--" + boundary + "--"; | |||
// multipart parsing, supports only one file for request, but that is | |||
// fine for our current terminal | |||
// Create a new file upload handler | |||
final FileUpload upload = createFileUpload(); | |||
final InputStream inputStream = request.getInputStream(); | |||
final UploadProgressListener pl = new UploadProgressListener(); | |||
int contentLength = request.getContentLength(); | |||
upload.setProgressListener(pl); | |||
boolean atStart = false; | |||
// Parse the request | |||
FileItemIterator iter; | |||
String rawfilename = "unknown"; | |||
String rawMimeType = "application/octet-stream"; | |||
try { | |||
iter = getUploadItemIterator(upload, request); | |||
/* | |||
* ATM this loop is run only once as we are uploading one file per | |||
* request. | |||
/* | |||
* 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.length() + 2); | |||
if (readLine.startsWith("Content-Disposition:") | |||
&& readLine.indexOf("filename=") > 0) { | |||
rawfilename = readLine.replaceAll(".*filename=", ""); | |||
String parenthesis = rawfilename.substring(0, 1); | |||
rawfilename = rawfilename.substring(1); | |||
rawfilename = rawfilename.substring(0, | |||
rawfilename.indexOf(parenthesis)); | |||
} else if (readLine.equals("")) { | |||
atStart = true; | |||
} else if (readLine.startsWith("Content-Type")) { | |||
rawMimeType = readLine.split(": ")[1]; | |||
} | |||
} | |||
contentLength -= (boundary.length() + 2); // 2 == CRLF | |||
final char[] charArray = boundary.toCharArray(); | |||
/* | |||
* Reads bytes from the underlaying stream. Compares the read bytes to | |||
* the boundary string and returns -1 if met. | |||
* | |||
* The maching 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. | |||
*/ | |||
InputStream simpleMultiPartReader = new InputStream() { | |||
int matchedCount = 0; | |||
int curBoundaryIndex = 0; | |||
/** | |||
* The byte found after a "promising start for boundary" | |||
*/ | |||
while (iter.hasNext()) { | |||
final FileItemStream item = iter.next(); | |||
final String name = item.getFieldName(); | |||
// Should report only the filename even if the browser sends the | |||
// path | |||
final String filename = removePath(item.getName()); | |||
final String mimeType = item.getContentType(); | |||
final InputStream stream = item.openStream(); | |||
if (item.isFormField()) { | |||
// ignored, upload requests contains only files | |||
private int bufferedByte = -1; | |||
private boolean atTheEnd = false; | |||
@Override | |||
public int read() throws IOException { | |||
if (atTheEnd) { | |||
return -1; | |||
} else if (bufferedByte >= 0) { | |||
return getBuffered(); | |||
} else { | |||
final UploadStream upstream = new UploadStream() { | |||
public String getContentName() { | |||
return filename; | |||
int fromActualStream = inputStream.read(); | |||
if (fromActualStream == -1) { | |||
// unexpected end of stream | |||
throw new IOException( | |||
"The multipart stream ended unexpectedly"); | |||
} | |||
if (charArray[matchedCount] == fromActualStream) { | |||
while (true) { | |||
matchedCount++; | |||
if (matchedCount == charArray.length) { | |||
// reached the end of file | |||
atTheEnd = true; | |||
return -1; | |||
} | |||
fromActualStream = inputStream.read(); | |||
if (fromActualStream != charArray[matchedCount]) { | |||
// Did not found full boundary, cache the last | |||
// byte | |||
bufferedByte = fromActualStream; | |||
return getBuffered(); | |||
} | |||
} | |||
} | |||
return fromActualStream; | |||
} | |||
} | |||
public String getContentType() { | |||
return mimeType; | |||
} | |||
private int getBuffered() throws IOException { | |||
int b; | |||
if (matchedCount == 0) { | |||
b = bufferedByte; | |||
bufferedByte = -1; | |||
} else { | |||
b = charArray[curBoundaryIndex++]; | |||
if (curBoundaryIndex == matchedCount) { | |||
matchedCount = 0; | |||
curBoundaryIndex = 0; | |||
// next call for getBuffered will return the | |||
// bufferedByte, not from the char array. | |||
} | |||
} | |||
if (b == -1) { | |||
throw new IOException( | |||
"The multipart stream ended unexpectedly"); | |||
} | |||
return b; | |||
} | |||
}; | |||
public InputStream getStream() { | |||
return stream; | |||
} | |||
/* | |||
* Should report only the filename even if the browser sends the path | |||
*/ | |||
final String filename = removePath(rawfilename); | |||
final String mimeType = rawMimeType; | |||
public String getStreamName() { | |||
return "stream"; | |||
} | |||
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 componente was read-only"); | |||
} | |||
streamToReceiver(simpleMultiPartReader, receiver, owner, filename, | |||
mimeType, contentLength); | |||
} catch (Exception e) { | |||
synchronized (application) { | |||
handleChangeVariablesError(application, (Component) owner, e, | |||
new HashMap<String, Object>()); | |||
} | |||
} | |||
sendUploadResponse(request, response); | |||
}; | |||
} | |||
if (name.startsWith("XHRFILE")) { | |||
String[] split = item.getFieldName().substring(7) | |||
.split("\\."); | |||
DragAndDropWrapper ddw = (DragAndDropWrapper) idPaintableMap | |||
.get(split[0]); | |||
/** | |||
* Used to stream plain file post (aka XHR2.post(File)) | |||
* | |||
* @param request | |||
* @param response | |||
* @param receiver | |||
* @param owner | |||
* @param contentLength | |||
* @throws IOException | |||
*/ | |||
protected void doHandleXhrFilePost(Request request, Response response, | |||
Receiver receiver, ReceiverOwner owner, int contentLength) | |||
throws IOException { | |||
try { | |||
ddw.receiveFile(upstream, split[1]); | |||
} catch (UploadException e) { | |||
synchronized (application) { | |||
handleChangeVariablesError(application, ddw, e, | |||
new HashMap<String, Object>()); | |||
} | |||
} | |||
// 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 componente was read-only"); | |||
} | |||
streamToReceiver(stream, receiver, owner, filename, mimeType, | |||
contentLength); | |||
} catch (Exception e) { | |||
synchronized (application) { | |||
handleChangeVariablesError(application, (Component) owner, e, | |||
new HashMap<String, Object>()); | |||
} | |||
} | |||
sendUploadResponse(request, response); | |||
} | |||
} else { | |||
protected final void streamToReceiver(final InputStream in, | |||
Receiver receiver, ReceiverOwner source, String filename, | |||
String type, int contentLength) throws UploadException { | |||
if (receiver == null) { | |||
throw new IllegalStateException("Receiver for the post not found"); | |||
} | |||
int separatorPos = name.lastIndexOf("_"); | |||
final String pid = name.substring(0, separatorPos); | |||
final Upload uploadComponent = (Upload) idPaintableMap | |||
.get(pid); | |||
if (uploadComponent == null) { | |||
throw new FileUploadException( | |||
"Upload component not found"); | |||
} | |||
if (uploadComponent.isReadOnly()) { | |||
throw new FileUploadException( | |||
"Warning: ignored file upload because upload component is set as read-only"); | |||
} | |||
synchronized (application) { | |||
// put upload component into receiving state | |||
uploadComponent.startUpload(); | |||
} | |||
ReceivingController controller = source | |||
.getReceivingController(receiver); | |||
// tell UploadProgressListener which component is | |||
// receiving | |||
// file | |||
pl.setUpload(uploadComponent); | |||
final Application application = getApplication(); | |||
try { | |||
uploadComponent.receiveUpload(upstream); | |||
} catch (UploadException e) { | |||
// error happened while receiving file. Handle the | |||
// error in the same manner as it would have | |||
// happened in | |||
// variable change. | |||
synchronized (application) { | |||
handleChangeVariablesError(application, | |||
uploadComponent, e, | |||
new HashMap<String, Object>()); | |||
} | |||
} | |||
OutputStream out = null; | |||
try { | |||
boolean listenProgress; | |||
synchronized (application) { | |||
ReceivingStartedEvent startedEvent = new ReceivingStartedEventImpl( | |||
receiver, filename, type, contentLength); | |||
controller.uploadStarted(startedEvent); | |||
out = receiver.receiveUpload(filename, type); | |||
listenProgress = controller.listenProgress(); | |||
} | |||
// 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; | |||
int totalBytes = 0; | |||
ReceivingProgressedEventImpl progressEvent = new ReceivingProgressedEventImpl( | |||
receiver, filename, type, contentLength); | |||
while ((bytesReadToBuffer = in.read(buffer)) > 0) { | |||
out.write(buffer, 0, bytesReadToBuffer); | |||
totalBytes += bytesReadToBuffer; | |||
if (listenProgress) { | |||
// update progress if listener set and contentLength | |||
// received | |||
synchronized (application) { | |||
/* | |||
* Note, we are reusing the same progress event, not to | |||
* pollute VM with lots of objects. This might not help | |||
* though if the end user aggressively updates UI on | |||
* each onProgress. | |||
*/ | |||
progressEvent.setBytesReceived(totalBytes); | |||
controller.onProgress(progressEvent); | |||
} | |||
} | |||
if (controller.isInterrupted()) { | |||
throw new UploadInterruptedException(); | |||
} | |||
} | |||
// upload successful | |||
out.close(); | |||
ReceivingEndedEvent event = new ReceivingEndedEventImpl(receiver, | |||
filename, type, totalBytes); | |||
synchronized (application) { | |||
controller.uploadFinished(event); | |||
} | |||
} catch (UploadInterruptedException e) { | |||
// Download interrupted by application code | |||
try { | |||
// still try to close output stream (e.g. file handle) | |||
out.close(); | |||
} catch (IOException e1) { | |||
// NOP | |||
} | |||
ReceivingFailedEvent event = new ReceivingFailedEventImpl(receiver, | |||
filename, type, contentLength, e); | |||
synchronized (application) { | |||
controller.uploadFailed(event); | |||
} | |||
// Note, we are not throwing interrupted exception forward as it is | |||
// not a terminal level erro like all other exception. | |||
} catch (final Exception e) { | |||
synchronized (application) { | |||
ReceivingFailedEvent event = new ReceivingFailedEventImpl( | |||
receiver, filename, type, contentLength, e); | |||
synchronized (application) { | |||
controller.uploadFailed(event); | |||
} | |||
// throw exception for terminal to be handled (to be passed to | |||
// terminalErrorHandler) | |||
throw new UploadException(e); | |||
} | |||
} catch (final FileUploadException e) { | |||
throw e; | |||
} | |||
sendUploadResponse(request, response); | |||
} | |||
/** | |||
@@ -722,14 +901,19 @@ public abstract class AbstractCommunicationManager implements | |||
} else { | |||
// remove detached components from paintableIdMap so they | |||
// can be GC'ed | |||
/* | |||
* TODO figure out if we could move this beyond the painting | |||
* phase, "respond as fast as possible, then do the cleanup". | |||
* Beware of painting the dirty detatched components. | |||
*/ | |||
for (Iterator<Paintable> it = paintableIdMap.keySet() | |||
.iterator(); it.hasNext();) { | |||
Component p = (Component) it.next(); | |||
if (p.getApplication() == null) { | |||
unregisterPaintable(p); | |||
idPaintableMap.remove(paintableIdMap.get(p)); | |||
it.remove(); | |||
dirtyPaintables.remove(p); | |||
p.removeListener(this); | |||
} | |||
} | |||
paintables = getDirtyVisibleComponents(window); | |||
@@ -977,6 +1161,16 @@ public abstract class AbstractCommunicationManager implements | |||
} | |||
/** | |||
* Called when communication manager stops listening for repaints for given | |||
* component. | |||
* | |||
* @param p | |||
*/ | |||
protected void unregisterPaintable(Component p) { | |||
p.removeListener(this); | |||
} | |||
/** | |||
* TODO document | |||
* | |||
@@ -1148,7 +1342,7 @@ public abstract class AbstractCommunicationManager implements | |||
return success; | |||
} | |||
private VariableOwner getVariableOwner(String string) { | |||
protected VariableOwner getVariableOwner(String string) { | |||
VariableOwner owner = (VariableOwner) idPaintableMap.get(string); | |||
if (owner == null && string.startsWith("DD")) { | |||
return getDragAndDropService(); | |||
@@ -1807,30 +2001,6 @@ public abstract class AbstractCommunicationManager implements | |||
} | |||
} | |||
/* | |||
* 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 | |||
* | |||
@@ -1964,4 +2134,7 @@ public abstract class AbstractCommunicationManager implements | |||
} | |||
} | |||
abstract String createReceiverUrl(ReceiverOwner owner, String name, | |||
Receiver value); | |||
} |
@@ -0,0 +1,49 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingEvent; | |||
/** | |||
* Abstract base class for ReceivingEvent implementations. | |||
*/ | |||
@SuppressWarnings("serial") | |||
abstract class AbstractReceivingEvent implements ReceivingEvent { | |||
private final String type; | |||
private final String filename; | |||
private Receiver receiver; | |||
private long contentLength; | |||
private long bytesReceived; | |||
public String getFileName() { | |||
return filename; | |||
} | |||
public String getMimeType() { | |||
return type; | |||
} | |||
public AbstractReceivingEvent(Receiver receiver, String filename, | |||
String type, long length) { | |||
this.receiver = receiver; | |||
this.filename = filename; | |||
this.type = type; | |||
contentLength = length; | |||
} | |||
public Receiver getReceiver() { | |||
return receiver; | |||
} | |||
public long getContentLength() { | |||
return contentLength; | |||
} | |||
public long getBytesReceived() { | |||
return bytesReceived; | |||
} | |||
void setBytesReceived(long bytesReceived) { | |||
this.bytesReceived = bytesReceived; | |||
} | |||
} |
@@ -7,6 +7,9 @@ package com.vaadin.terminal.gwt.server; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.UUID; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
@@ -14,12 +17,12 @@ import javax.servlet.http.HttpServletResponse; | |||
import javax.servlet.http.HttpSession; | |||
import com.vaadin.Application; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUpload; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; | |||
import com.vaadin.external.org.apache.commons.fileupload.servlet.ServletFileUpload; | |||
import com.vaadin.terminal.ApplicationResource; | |||
import com.vaadin.terminal.DownloadStream; | |||
import com.vaadin.terminal.Paintable; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.Window; | |||
/** | |||
@@ -202,34 +205,56 @@ public class CommunicationManager extends AbstractCommunicationManager { | |||
super(application); | |||
} | |||
@Override | |||
protected FileUpload createFileUpload() { | |||
return new ServletFileUpload(); | |||
} | |||
@Override | |||
protected FileItemIterator getUploadItemIterator(FileUpload upload, | |||
Request request) throws IOException, FileUploadException { | |||
return ((ServletFileUpload) upload) | |||
.getItemIterator((HttpServletRequest) request | |||
.getWrappedRequest()); | |||
} | |||
/** | |||
* Handles file upload request submitted via Upload component. | |||
* | |||
* TODO document | |||
* @see #createReceiverUrl(ReceiverOwner, String, Receiver) | |||
* | |||
* @param request | |||
* @param response | |||
* @throws IOException | |||
* @throws FileUploadException | |||
* @throws InvalidUIDLSecurityKeyException | |||
*/ | |||
public void handleFileUpload(HttpServletRequest request, | |||
HttpServletResponse response) throws IOException, | |||
FileUploadException { | |||
doHandleFileUpload(new HttpServletRequestWrapper(request), | |||
new HttpServletResponseWrapper(response)); | |||
InvalidUIDLSecurityKeyException { | |||
/* | |||
* URI pattern: APP/UPPLOAD/[PID]/[NAME]/[SECKEY] See #createReceiverUrl | |||
*/ | |||
String pathInfo = request.getPathInfo(); | |||
// strip away part until the data we are interested starts | |||
int startOfData = pathInfo | |||
.indexOf(AbstractApplicationServlet.UPLOAD_URL_PREFIX) | |||
+ AbstractApplicationServlet.UPLOAD_URL_PREFIX.length(); | |||
String uppUri = pathInfo.substring(startOfData); | |||
String[] parts = uppUri.split("/", 3); // 0 = pid, 1= name, 2 = sec key | |||
Receiver receiver = pidToNameToReceiver.get(parts[0]).remove(parts[1]); | |||
String secKey = receiverToSeckey.remove(receiver); | |||
if (secKey.equals(parts[2])) { | |||
ReceiverOwner source = (ReceiverOwner) getVariableOwner(parts[0]); | |||
String contentType = request.getContentType(); | |||
if (request.getContentType().contains("boundary")) { | |||
// Multipart requests contain boundary string | |||
doHandleSimpleMultipartFileUpload( | |||
new HttpServletRequestWrapper(request), | |||
new HttpServletResponseWrapper(response), receiver, | |||
source, contentType.split("boundary=")[1]); | |||
} else { | |||
// if boundary string does not exist, the posted file is from | |||
// XHR2.post(File) | |||
doHandleXhrFilePost(new HttpServletRequestWrapper(request), | |||
new HttpServletResponseWrapper(response), receiver, | |||
source, request.getContentLength()); | |||
} | |||
} else { | |||
throw new InvalidUIDLSecurityKeyException( | |||
"Security key in upload post did not match!"); | |||
} | |||
} | |||
/** | |||
@@ -308,4 +333,65 @@ public class CommunicationManager extends AbstractCommunicationManager { | |||
new AbstractApplicationServletWrapper(applicationServlet)); | |||
} | |||
@Override | |||
protected void unregisterPaintable(Component p) { | |||
/* Cleanup possible receivers */ | |||
if (pidToNameToReceiver != null && p instanceof ReceiverOwner) { | |||
Map<String, Receiver> removed = pidToNameToReceiver | |||
.remove(getPaintableId(p)); | |||
if (removed != null) { | |||
for (String key : removed.keySet()) { | |||
receiverToSeckey.remove(removed.get(key)); | |||
} | |||
} | |||
} | |||
super.unregisterPaintable(p); | |||
} | |||
private Map<String, Map<String, Receiver>> pidToNameToReceiver; | |||
private Map<Receiver, String> receiverToSeckey; | |||
@Override | |||
String createReceiverUrl(ReceiverOwner owner, String name, Receiver value) { | |||
/* | |||
* We will use the same APP/* URI space as ApplicationResources but | |||
* prefix url with UPLOAD | |||
* | |||
* eg. APP/UPPLOAD/[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 Receiver when handling | |||
* post | |||
*/ | |||
String paintableId = getPaintableId((Paintable) owner); | |||
String key = paintableId + "/" + name; | |||
if (pidToNameToReceiver == null) { | |||
pidToNameToReceiver = new HashMap<String, Map<String, Receiver>>(); | |||
} | |||
Map<String, Receiver> nameToReceiver = pidToNameToReceiver | |||
.get(paintableId); | |||
if (nameToReceiver == null) { | |||
nameToReceiver = new HashMap<String, Receiver>(); | |||
pidToNameToReceiver.put(paintableId, nameToReceiver); | |||
} | |||
nameToReceiver.put(name, value); | |||
if (receiverToSeckey == null) { | |||
receiverToSeckey = new HashMap<Receiver, String>(); | |||
} | |||
String seckey = UUID.randomUUID().toString(); | |||
receiverToSeckey.put(value, seckey); | |||
return getApplication().getURL() | |||
+ AbstractApplicationServlet.UPLOAD_URL_PREFIX + key + "/" | |||
+ seckey; | |||
} | |||
} |
@@ -27,6 +27,8 @@ import com.vaadin.terminal.ExternalResource; | |||
import com.vaadin.terminal.PaintException; | |||
import com.vaadin.terminal.PaintTarget; | |||
import com.vaadin.terminal.Paintable; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.terminal.Resource; | |||
import com.vaadin.terminal.ThemeResource; | |||
import com.vaadin.terminal.VariableOwner; | |||
@@ -1102,4 +1104,11 @@ public class JsonPaintTarget implements PaintTarget { | |||
Collection<Class<? extends Paintable>> getUsedPaintableTypes() { | |||
return usedPaintableTypes; | |||
} | |||
public void addVariable(ReceiverOwner owner, String name, Receiver value) | |||
throws PaintException { | |||
String url = manager.createReceiverUrl(owner, name, value); | |||
addVariable(owner, name, url); | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
@SuppressWarnings("serial") | |||
public class NoInputStreamException extends Exception { | |||
} |
@@ -0,0 +1,6 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
@SuppressWarnings("serial") | |||
public class NoOutputStreamException extends Exception { | |||
} |
@@ -250,6 +250,7 @@ public class PortletApplicationContext2 extends AbstractWebApplicationContext { | |||
return resourceURL.toString(); | |||
} else { | |||
// in a background thread or otherwise outside a request | |||
// TODO exception ?? | |||
return null; | |||
} | |||
} |
@@ -7,9 +7,9 @@ import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.lang.reflect.Method; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import javax.portlet.ActionRequest; | |||
import javax.portlet.ActionResponse; | |||
import javax.portlet.ClientDataRequest; | |||
import javax.portlet.MimeResponse; | |||
import javax.portlet.PortletRequest; | |||
@@ -17,15 +17,16 @@ import javax.portlet.PortletResponse; | |||
import javax.portlet.PortletSession; | |||
import javax.portlet.ResourceRequest; | |||
import javax.portlet.ResourceResponse; | |||
import javax.portlet.ResourceURL; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequestWrapper; | |||
import com.vaadin.Application; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileItemIterator; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUpload; | |||
import com.vaadin.external.org.apache.commons.fileupload.FileUploadException; | |||
import com.vaadin.external.org.apache.commons.fileupload.portlet.PortletFileUpload; | |||
import com.vaadin.terminal.DownloadStream; | |||
import com.vaadin.terminal.Paintable; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.Window; | |||
/** | |||
@@ -37,7 +38,7 @@ import com.vaadin.ui.Window; | |||
@SuppressWarnings("serial") | |||
public class PortletCommunicationManager extends AbstractCommunicationManager { | |||
protected String dummyURL; | |||
private ResourceResponse currentUidlResponse; | |||
private static class PortletRequestWrapper implements Request { | |||
@@ -191,39 +192,36 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { | |||
super(application); | |||
} | |||
@Override | |||
protected FileUpload createFileUpload() { | |||
return new PortletFileUpload(); | |||
} | |||
@Override | |||
protected FileItemIterator getUploadItemIterator(FileUpload upload, | |||
Request request) throws IOException, FileUploadException { | |||
return ((PortletFileUpload) upload) | |||
.getItemIterator((ActionRequest) request.getWrappedRequest()); | |||
} | |||
public void handleFileUpload(ResourceRequest request, | |||
ResourceResponse response) throws IOException { | |||
String contentType = request.getContentType(); | |||
String name = request.getParameter("name"); | |||
String ownerId = request.getParameter("rec-owner"); | |||
ReceiverOwner variableOwner = (ReceiverOwner) getVariableOwner(ownerId); | |||
Receiver receiver = ownerToNameToReceiver.get(variableOwner).remove( | |||
name); | |||
// clean up, may be re added on next paint | |||
ownerToNameToReceiver.get(variableOwner).remove(name); | |||
if (contentType.contains("boundary")) { | |||
doHandleSimpleMultipartFileUpload( | |||
new PortletRequestWrapper(request), | |||
new PortletResponseWrapper(response), receiver, | |||
variableOwner, contentType.split("boundary=")[1]); | |||
} else { | |||
doHandleXhrFilePost(new PortletRequestWrapper(request), | |||
new PortletResponseWrapper(response), receiver, | |||
variableOwner, request.getContentLength()); | |||
} | |||
public void handleFileUpload(ActionRequest request, ActionResponse response) | |||
throws FileUploadException, IOException { | |||
doHandleFileUpload(new PortletRequestWrapper(request), | |||
new PortletResponseWrapper(response)); | |||
} | |||
@Override | |||
protected void sendUploadResponse(Request request, Response response) | |||
throws IOException { | |||
if (response.getWrappedResponse() instanceof ActionResponse) { | |||
/* | |||
* If we do not redirect to some other page, the entire portal page | |||
* will be re-printed into the target of the upload request (an | |||
* IFRAME), which in turn will cause very strange side effects. | |||
*/ | |||
System.out.println("Redirecting to dummyURL: " + dummyURL); | |||
((ActionResponse) response.getWrappedResponse()) | |||
.sendRedirect(dummyURL == null ? "http://www.google.com" | |||
: dummyURL); | |||
} else { | |||
super.sendUploadResponse(request, response); | |||
protected void unregisterPaintable(Component p) { | |||
super.unregisterPaintable(p); | |||
if (ownerToNameToReceiver != null) { | |||
ownerToNameToReceiver.remove(p); | |||
} | |||
} | |||
@@ -231,10 +229,12 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { | |||
ResourceResponse response, | |||
AbstractApplicationPortlet applicationPortlet, Window window) | |||
throws InvalidUIDLSecurityKeyException, IOException { | |||
currentUidlResponse = response; | |||
doHandleUidlRequest(new PortletRequestWrapper(request), | |||
new PortletResponseWrapper(response), | |||
new AbstractApplicationPortletWrapper(applicationPortlet), | |||
window); | |||
currentUidlResponse = null; | |||
} | |||
DownloadStream handleURI(Window window, ResourceRequest request, | |||
@@ -265,9 +265,32 @@ public class PortletCommunicationManager extends AbstractCommunicationManager { | |||
Window getApplicationWindow(PortletRequest request, | |||
AbstractApplicationPortlet applicationPortlet, | |||
Application application, Window assumedWindow) { | |||
return doGetApplicationWindow(new PortletRequestWrapper(request), | |||
new AbstractApplicationPortletWrapper(applicationPortlet), | |||
application, assumedWindow); | |||
} | |||
private Map<ReceiverOwner, Map<String, Receiver>> ownerToNameToReceiver; | |||
@Override | |||
String createReceiverUrl(ReceiverOwner owner, String name, Receiver value) { | |||
if (ownerToNameToReceiver == null) { | |||
ownerToNameToReceiver = new HashMap<ReceiverOwner, Map<String, Receiver>>(); | |||
} | |||
Map<String, Receiver> nameToReceiver = ownerToNameToReceiver.get(owner); | |||
if (nameToReceiver == null) { | |||
nameToReceiver = new HashMap<String, Receiver>(); | |||
ownerToNameToReceiver.put(owner, nameToReceiver); | |||
} | |||
nameToReceiver.put(name, value); | |||
ResourceURL resurl = currentUidlResponse.createResourceURL(); | |||
resurl.setResourceID("UPLOAD"); | |||
resurl.setParameter("name", name); | |||
resurl.setParameter("rec-owner", getPaintableId((Paintable) owner)); | |||
resurl.setProperty("name", name); | |||
resurl.setProperty("rec-owner", getPaintableId((Paintable) owner)); | |||
return resurl.toString(); | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; | |||
@SuppressWarnings("serial") | |||
class ReceivingEndedEventImpl extends AbstractReceivingEvent implements | |||
ReceivingEndedEvent { | |||
public ReceivingEndedEventImpl(Receiver receiver, String filename, | |||
String type, long totalBytes) { | |||
super(receiver, filename, type, totalBytes); | |||
} | |||
} |
@@ -0,0 +1,22 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; | |||
@SuppressWarnings("serial") | |||
class ReceivingFailedEventImpl extends AbstractReceivingEvent implements | |||
ReceivingFailedEvent { | |||
private final Exception exception; | |||
public ReceivingFailedEventImpl(Receiver receiver, final String filename, | |||
final String type, long contentLength, final Exception exception) { | |||
super(receiver, filename, type, contentLength); | |||
this.exception = exception; | |||
} | |||
public Exception getException() { | |||
return exception; | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; | |||
@SuppressWarnings("serial") | |||
class ReceivingProgressedEventImpl extends AbstractReceivingEvent implements | |||
ReceivingProgressedEvent { | |||
public ReceivingProgressedEventImpl(Receiver receiver, | |||
final String filename, final String type, long contentLength) { | |||
super(receiver, filename, type, contentLength); | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; | |||
@SuppressWarnings("serial") | |||
class ReceivingStartedEventImpl extends AbstractReceivingEvent implements | |||
ReceivingStartedEvent { | |||
public ReceivingStartedEventImpl(Receiver receiver, final String filename, | |||
final String type, long contentLength) { | |||
super(receiver, filename, type, contentLength); | |||
} | |||
} |
@@ -0,0 +1,12 @@ | |||
package com.vaadin.terminal.gwt.server; | |||
@SuppressWarnings("serial") | |||
public class UploadException extends Exception { | |||
public UploadException(Exception e) { | |||
super("Upload failed", e); | |||
} | |||
public UploadException(String msg) { | |||
super(msg); | |||
} | |||
} |
@@ -3,10 +3,6 @@ | |||
*/ | |||
package com.vaadin.ui; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.io.Serializable; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
@@ -19,23 +15,34 @@ import com.vaadin.event.dd.TargetDetails; | |||
import com.vaadin.event.dd.TargetDetailsImpl; | |||
import com.vaadin.terminal.PaintException; | |||
import com.vaadin.terminal.PaintTarget; | |||
import com.vaadin.terminal.UploadStream; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.terminal.gwt.client.MouseEventDetails; | |||
import com.vaadin.terminal.gwt.client.ui.VDragAndDropWrapper; | |||
import com.vaadin.terminal.gwt.client.ui.dd.HorizontalDropLocation; | |||
import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; | |||
import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; | |||
import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; | |||
import com.vaadin.ui.Upload.Receiver; | |||
import com.vaadin.ui.Upload.UploadException; | |||
import com.vaadin.ui.Html5File.ProxyReceiver; | |||
@SuppressWarnings("serial") | |||
@ClientWidget(VDragAndDropWrapper.class) | |||
public class DragAndDropWrapper extends CustomComponent implements DropTarget, | |||
DragSource { | |||
DragSource, ReceiverOwner { | |||
public class WrapperTransferable extends TransferableImpl { | |||
/** | |||
* @deprecated this class is made top level in recent version. Use | |||
* com.vaadin.ui.Html5File instead | |||
*/ | |||
@Deprecated | |||
private class Html5File extends com.vaadin.ui.Html5File { | |||
Html5File(String name, long size, String mimeType) { | |||
super(name, size, mimeType); | |||
} | |||
} | |||
private Html5File[] files; | |||
public WrapperTransferable(Component sourceComponent, | |||
@@ -45,13 +52,14 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, | |||
if (fc != null) { | |||
files = new Html5File[fc]; | |||
for (int i = 0; i < fc; i++) { | |||
Html5File file = new Html5File(); | |||
Html5File file = new Html5File( | |||
(String) rawVariables.get("fn" + i), // name | |||
(Integer) rawVariables.get("fs" + i), // size | |||
(String) rawVariables.get("ft" + i)); // mime | |||
String id = (String) rawVariables.get("fi" + i); | |||
file.name = (String) rawVariables.get("fn" + i); | |||
file.size = (Integer) rawVariables.get("fs" + i); | |||
file.type = (String) rawVariables.get("ft" + i); | |||
files[i] = file; | |||
receivers.put(id, file); | |||
requestRepaint(); // paint Receivers | |||
} | |||
} | |||
} | |||
@@ -96,50 +104,6 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, | |||
return data; | |||
} | |||
/** | |||
* {@link DragAndDropWrapper} can receive also files from client | |||
* computer if appropriate HTML 5 features are supported on client side. | |||
* This class wraps information about dragged file on server side. | |||
*/ | |||
public class Html5File implements Serializable { | |||
public String name; | |||
private int size; | |||
private Receiver receiver; | |||
private String type; | |||
public String getFileName() { | |||
return name; | |||
} | |||
public int getFileSize() { | |||
return size; | |||
} | |||
public String getType() { | |||
return type; | |||
} | |||
/** | |||
* Sets the {@link Receiver} that into which the file contents will | |||
* be written. Usage of Reveiver is similar to {@link Upload} | |||
* component. | |||
* | |||
* <p> | |||
* <em>Note!</em> receiving file contents is experimental feature | |||
* depending on HTML 5 API's. It is supported only by Firefox 3.6 at | |||
* this time. | |||
* | |||
* @param receiver | |||
* the callback that returns stream where the | |||
* implementation writes the file contents as it arrives. | |||
*/ | |||
public void setReceiver(Receiver receiver) { | |||
this.receiver = receiver; | |||
} | |||
} | |||
} | |||
private Map<String, Html5File> receivers = new HashMap<String, Html5File>(); | |||
@@ -220,9 +184,23 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, | |||
if (getDropHandler() != null) { | |||
getDropHandler().getAcceptCriterion().paint(target); | |||
} | |||
if (receivers != null && receivers.size() > 0) { | |||
for (String id : receivers.keySet()) { | |||
Html5File html5File = receivers.get(id); | |||
if (html5File.getReceiver() != null) { | |||
target.addVariable(this, "rec-" + id, | |||
html5File.getProxyReceiver()); | |||
} else { | |||
// instructs the client side not to send the file | |||
target.addVariable(this, "rec-" + id, (String) null); | |||
} | |||
} | |||
} | |||
} | |||
private DropHandler dropHandler; | |||
private Html5File currentlyUploadedFile; | |||
private boolean listenProgressOfUploadedFile; | |||
public DropHandler getDropHandler() { | |||
return dropHandler; | |||
@@ -251,38 +229,96 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, | |||
return dragStartMode; | |||
} | |||
/** | |||
* This method should only be used by Vaadin terminal implementation. This | |||
* is not end user api. | |||
* | |||
* TODO should fire progress events + end/succes events like upload. Not | |||
* critical until we have a wider browser support for HTML5 File API | |||
* | |||
* @param upstream | |||
* @param fileId | |||
* @throws UploadException | |||
/* | |||
* Single controller is enough for atm as files are transferred in serial. | |||
* If parallel transfer is needed, this logic needs to go to Html5File | |||
*/ | |||
public void receiveFile(UploadStream upstream, String fileId) | |||
throws UploadException { | |||
Html5File file = receivers.get(fileId); | |||
if (file != null && file.receiver != null) { | |||
OutputStream receiveUpload = file.receiver.receiveUpload( | |||
file.getFileName(), "TODO"); | |||
InputStream stream = upstream.getStream(); | |||
byte[] buf = new byte[AbstractApplicationServlet.MAX_BUFFER_SIZE]; | |||
int bytesRead; | |||
try { | |||
while ((bytesRead = stream.read(buf)) != -1) { | |||
receiveUpload.write(buf, 0, bytesRead); | |||
private ReceivingController controller = new ReceivingController() { | |||
/* | |||
* With XHR2 file posts we can't provide as much information from the | |||
* terminal as with multipart request. This helper class wraps the | |||
* terminal event and provides the lacking information from the | |||
* Html5File. | |||
*/ | |||
class ReceivingEventWrapper implements ReceivingFailedEvent, | |||
ReceivingEndedEvent, ReceivingStartedEvent, | |||
ReceivingProgressedEvent { | |||
private ReceivingEvent wrappedEvent; | |||
ReceivingEventWrapper(ReceivingEvent e) { | |||
wrappedEvent = e; | |||
} | |||
public String getMimeType() { | |||
return currentlyUploadedFile.getType(); | |||
} | |||
public String getFileName() { | |||
return currentlyUploadedFile.getFileName(); | |||
} | |||
public long getContentLength() { | |||
return currentlyUploadedFile.getFileSize(); | |||
} | |||
public Receiver getReceiver() { | |||
return currentlyUploadedFile.getReceiver(); | |||
} | |||
public Exception getException() { | |||
if (wrappedEvent instanceof ReceivingFailedEvent) { | |||
return ((ReceivingFailedEvent) wrappedEvent).getException(); | |||
} | |||
receiveUpload.close(); | |||
} catch (IOException e) { | |||
throw new UploadException(e); | |||
return null; | |||
} | |||
public long getBytesReceived() { | |||
return wrappedEvent.getBytesReceived(); | |||
} | |||
} | |||
public boolean listenProgress() { | |||
return listenProgressOfUploadedFile; | |||
} | |||
public void onProgress(ReceivingProgressedEvent event) { | |||
currentlyUploadedFile.getUploadListener().onProgress( | |||
new ReceivingEventWrapper(event)); | |||
} | |||
public void uploadStarted(ReceivingStartedEvent event) { | |||
currentlyUploadedFile = ((ProxyReceiver) event.getReceiver()) | |||
.getFile(); | |||
listenProgressOfUploadedFile = currentlyUploadedFile | |||
.getUploadListener() != null; | |||
if (listenProgressOfUploadedFile) { | |||
currentlyUploadedFile.getUploadListener().uploadStarted( | |||
new ReceivingEventWrapper(event)); | |||
} | |||
} | |||
public void uploadFinished(ReceivingEndedEvent event) { | |||
if (listenProgressOfUploadedFile) { | |||
currentlyUploadedFile.getUploadListener().uploadFinished( | |||
new ReceivingEventWrapper(event)); | |||
} | |||
// clean up the reference when file is downloaded | |||
receivers.remove(fileId); | |||
} | |||
public void uploadFailed(final ReceivingFailedEvent event) { | |||
if (listenProgressOfUploadedFile) { | |||
currentlyUploadedFile.getUploadListener().uploadFailed( | |||
new ReceivingEventWrapper(event)); | |||
} | |||
} | |||
public boolean isInterrupted() { | |||
return currentlyUploadedFile.isInterrupted(); | |||
} | |||
}; | |||
public ReceivingController getReceivingController(Receiver receiver) { | |||
return controller; | |||
} | |||
} |
@@ -0,0 +1,137 @@ | |||
package com.vaadin.ui; | |||
import java.io.OutputStream; | |||
import java.io.Serializable; | |||
import com.vaadin.event.dd.DropHandler; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; | |||
/** | |||
* {@link DragAndDropWrapper} can receive also files from client computer if | |||
* appropriate HTML 5 features are supported on client side. This class wraps | |||
* information about dragged file on server side. | |||
*/ | |||
public class Html5File implements Serializable { | |||
final class ProxyReceiver implements Receiver { | |||
public OutputStream receiveUpload(String filename, String MIMEType) { | |||
if (receiver == null) { | |||
return null; | |||
} | |||
return receiver.receiveUpload(filename, MIMEType); | |||
} | |||
Html5File getFile() { | |||
return Html5File.this; | |||
} | |||
} | |||
private String name; | |||
private long size; | |||
private Receiver receiver; | |||
private String type; | |||
Html5File(String name, long size, String mimeType) { | |||
this.name = name; | |||
this.size = size; | |||
type = mimeType; | |||
} | |||
/** | |||
* The receiver that is registered to the terminal. Wraps the actual | |||
* Receiver set later by Html5File user. | |||
*/ | |||
private ProxyReceiver proxyReceiver = new ProxyReceiver(); | |||
private boolean interrupted = false; | |||
private Html5FileUploadListener listener;; | |||
public String getFileName() { | |||
return name; | |||
} | |||
public long getFileSize() { | |||
return size; | |||
} | |||
public String getType() { | |||
return type; | |||
} | |||
/** | |||
* Sets the {@link Receiver} that into which the file contents will be | |||
* written. Usage of Reveiver is similar to {@link Upload} component. | |||
* <p> | |||
* If the {@link Receiver} is not set in the {@link DropHandler} the file | |||
* contents will not be sent to server. | |||
* <p> | |||
* <em>Note!</em> receiving file contents is experimental feature depending | |||
* on HTML 5 API's. It is supported only by modern web brosers like Firefox | |||
* 3.6 and above and recent webkit based browsers (Safari 5, Chrome 6) at | |||
* this time. | |||
* | |||
* @param receiver | |||
* the callback that returns stream where the implementation | |||
* writes the file contents as it arrives. | |||
*/ | |||
public void setReceiver(Receiver receiver) { | |||
this.receiver = receiver; | |||
} | |||
public Receiver getReceiver() { | |||
return receiver; | |||
} | |||
ProxyReceiver getProxyReceiver() { | |||
return proxyReceiver; | |||
} | |||
/** | |||
* Gets the {@link Html5FileUploadListener} that is used to track the progress of | |||
* streaming the file contents to given {@link Receiver}. | |||
* | |||
* @return | |||
*/ | |||
public Html5FileUploadListener getUploadListener() { | |||
return listener; | |||
} | |||
/** | |||
* Sets the {@link Html5FileUploadListener} that can be used to track the progress of | |||
* streaming the file contents to given {@link Receiver}. | |||
* | |||
* @param listener | |||
* @see #setReceiver(Receiver) | |||
*/ | |||
public void setUploadListener(Html5FileUploadListener listener) { | |||
this.listener = listener; | |||
} | |||
public boolean isInterrupted() { | |||
return interrupted; | |||
} | |||
/** | |||
* Interrupts uploading this file. | |||
* | |||
* @param interrupted | |||
*/ | |||
public void setInterrupted(boolean interrupted) { | |||
this.interrupted = interrupted; | |||
} | |||
public interface Html5FileUploadListener { | |||
void onProgress(ReceivingProgressedEvent event); | |||
void uploadStarted(ReceivingStartedEvent event); | |||
void uploadFinished(ReceivingEndedEvent event); | |||
void uploadFailed(ReceivingFailedEvent event); | |||
} | |||
} |
@@ -4,20 +4,18 @@ | |||
package com.vaadin.ui; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.io.Serializable; | |||
import java.lang.reflect.Method; | |||
import java.util.Iterator; | |||
import java.util.LinkedHashSet; | |||
import java.util.Map; | |||
import com.vaadin.Application; | |||
import com.vaadin.terminal.PaintException; | |||
import com.vaadin.terminal.PaintTarget; | |||
import com.vaadin.terminal.UploadStream; | |||
import com.vaadin.terminal.ReceiverOwner; | |||
import com.vaadin.terminal.gwt.client.ui.VUpload; | |||
import com.vaadin.terminal.gwt.server.NoInputStreamException; | |||
import com.vaadin.terminal.gwt.server.NoOutputStreamException; | |||
import com.vaadin.ui.ClientWidget.LoadStyle; | |||
/** | |||
@@ -62,12 +60,8 @@ import com.vaadin.ui.ClientWidget.LoadStyle; | |||
*/ | |||
@SuppressWarnings("serial") | |||
@ClientWidget(value = VUpload.class, loadStyle = LoadStyle.LAZY) | |||
public class Upload extends AbstractComponent implements Component.Focusable { | |||
/** | |||
* Upload buffer size. | |||
*/ | |||
private static final int BUFFER_SIZE = 64 * 1024; // 64k | |||
public class Upload extends AbstractComponent implements Component.Focusable, | |||
ReceiverOwner { | |||
/** | |||
* Should the field be focused on next repaint? | |||
@@ -130,105 +124,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
receiver = uploadReceiver; | |||
} | |||
/** | |||
* This method is called by terminal when upload is received. | |||
* | |||
* Note, this method is called outside synchronized (Application) block, so | |||
* overriding this may be dangerous. | |||
* | |||
* @param upload | |||
*/ | |||
public void receiveUpload(UploadStream upload) throws UploadException { | |||
if (receiver == null) { | |||
throw new IllegalStateException( | |||
"Receiver not set for the Upload component"); | |||
} | |||
if (!isUploading) { | |||
throw new IllegalStateException("uploading not started"); | |||
} | |||
// Gets file properties | |||
final String filename = upload.getContentName(); | |||
final String type = upload.getContentType(); | |||
final Application application = getApplication(); | |||
synchronized (application) { | |||
fireStarted(filename, type); | |||
} | |||
// Gets the output target stream | |||
final OutputStream out = receiver.receiveUpload(filename, type); | |||
if (out == null) { | |||
synchronized (application) { | |||
fireNoOutputStream(filename, type, 0); | |||
endUpload(); | |||
} | |||
return; | |||
} | |||
final InputStream in = upload.getStream(); | |||
if (null == in) { | |||
// No file, for instance non-existent filename in html upload | |||
synchronized (application) { | |||
fireNoInputStream(filename, type, 0); | |||
endUpload(); | |||
} | |||
return; | |||
} | |||
final byte buffer[] = new byte[BUFFER_SIZE]; | |||
int bytesRead = 0; | |||
totalBytes = 0; | |||
try { | |||
while ((bytesRead = in.read(buffer)) > 0) { | |||
out.write(buffer, 0, bytesRead); | |||
totalBytes += bytesRead; | |||
if (contentLength > 0 | |||
&& (progressListeners != null || progressListener != null)) { | |||
// update progress if listener set and contentLength | |||
// received | |||
synchronized (application) { | |||
fireUpdateProgress(totalBytes, contentLength); | |||
} | |||
} | |||
if (interrupted) { | |||
throw new UploadInterruptedException(); | |||
} | |||
} | |||
// upload successful | |||
out.close(); | |||
synchronized (application) { | |||
fireUploadSuccess(filename, type, totalBytes); | |||
endUpload(); | |||
requestRepaint(); | |||
} | |||
} catch (final Exception e) { | |||
synchronized (application) { | |||
if (e instanceof UploadInterruptedException) { | |||
// Download interrupted | |||
try { | |||
// still try to close output stream | |||
out.close(); | |||
} catch (IOException e1) { | |||
// NOP | |||
} | |||
} | |||
fireUploadInterrupted(filename, type, totalBytes, e); | |||
endUpload(); | |||
interrupted = false; | |||
if (!(e instanceof UploadInterruptedException)) { | |||
// throw exception for terminal to be handled | |||
throw new UploadException(e); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Invoked when the value of a variable has changed. | |||
* | |||
@@ -278,6 +173,9 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
target.addAttribute("nextid", nextid); | |||
// Post file to this receiver | |||
target.addVariable(this, "action", receiver); | |||
} | |||
/** | |||
@@ -288,20 +186,11 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @version | |||
* @VERSION@ | |||
* @since 3.0 | |||
* @deprecated use {@link com.vaadin.terminal.Receiver} instead. A "copy" | |||
* here is kept for backwards compatibility. | |||
*/ | |||
public interface Receiver extends Serializable { | |||
/** | |||
* Invoked when a new upload arrives. | |||
* | |||
* @param filename | |||
* the desired filename of the upload, usually as specified | |||
* by the client. | |||
* @param MIMEType | |||
* the MIME type of the uploaded file. | |||
* @return Stream to which the uploaded file should be written. | |||
*/ | |||
public OutputStream receiveUpload(String filename, String MIMEType); | |||
@Deprecated | |||
public interface Receiver extends com.vaadin.terminal.Receiver { | |||
} | |||
/* Upload events */ | |||
@@ -332,19 +221,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
} | |||
} | |||
private class UploadInterruptedException extends Exception { | |||
public UploadInterruptedException() { | |||
super("Upload interrupted by other thread"); | |||
} | |||
} | |||
public static class UploadException extends Exception { | |||
public UploadException(Exception e) { | |||
super("Upload failed", e); | |||
} | |||
} | |||
/** | |||
* Upload.Received event is sent when the upload receives a file, regardless | |||
* of whether the reception was successful or failed. If you wish to | |||
@@ -356,7 +232,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @VERSION@ | |||
* @since 3.0 | |||
*/ | |||
public class FinishedEvent extends Component.Event { | |||
public static class FinishedEvent extends Component.Event { | |||
/** | |||
* Length of the received file. | |||
@@ -439,7 +315,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @VERSION@ | |||
* @since 3.0 | |||
*/ | |||
public class FailedEvent extends FinishedEvent { | |||
public static class FailedEvent extends FinishedEvent { | |||
private Exception reason = null; | |||
@@ -484,7 +360,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
/** | |||
* FailedEvent that indicates that an output stream could not be obtained. | |||
*/ | |||
public class NoOutputStreamEvent extends FailedEvent { | |||
public static class NoOutputStreamEvent extends FailedEvent { | |||
/** | |||
* | |||
@@ -502,7 +378,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
/** | |||
* FailedEvent that indicates that an input stream could not be obtained. | |||
*/ | |||
public class NoInputStreamEvent extends FailedEvent { | |||
public static class NoInputStreamEvent extends FailedEvent { | |||
/** | |||
* | |||
@@ -526,7 +402,7 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @VERSION@ | |||
* @since 3.0 | |||
*/ | |||
public class SucceededEvent extends FinishedEvent { | |||
public static class SucceededEvent extends FinishedEvent { | |||
/** | |||
* | |||
@@ -550,10 +426,14 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @VERSION@ | |||
* @since 5.0 | |||
*/ | |||
public class StartedEvent extends Component.Event { | |||
public static class StartedEvent extends Component.Event { | |||
private final String filename; | |||
private final String type; | |||
/** | |||
* Length of the received file. | |||
*/ | |||
private final long length; | |||
/** | |||
* | |||
@@ -562,10 +442,12 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @param MIMEType | |||
* @param length | |||
*/ | |||
public StartedEvent(Upload source, String filename, String MIMEType) { | |||
public StartedEvent(Upload source, String filename, String MIMEType, | |||
long contentLength) { | |||
super(source); | |||
this.filename = filename; | |||
type = MIMEType; | |||
length = contentLength; | |||
} | |||
/** | |||
@@ -595,6 +477,13 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
return type; | |||
} | |||
/** | |||
* @return the length of the file that is being uploaded | |||
*/ | |||
public long getContentLength() { | |||
return length; | |||
} | |||
} | |||
/** | |||
@@ -786,19 +675,8 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
* @param length | |||
*/ | |||
protected void fireStarted(String filename, String MIMEType) { | |||
fireEvent(new Upload.StartedEvent(this, filename, MIMEType)); | |||
} | |||
/** | |||
* Emit upload finished event. | |||
* | |||
* @param filename | |||
* @param MIMEType | |||
* @param length | |||
*/ | |||
protected void fireUploadReceived(String filename, String MIMEType, | |||
long length) { | |||
fireEvent(new Upload.FinishedEvent(this, filename, MIMEType, length)); | |||
fireEvent(new Upload.StartedEvent(this, filename, MIMEType, | |||
contentLength)); | |||
} | |||
/** | |||
@@ -913,15 +791,6 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
this.tabIndex = tabIndex; | |||
} | |||
/** | |||
* Sets the size of the file currently being uploaded. | |||
* | |||
* @param contentLength | |||
*/ | |||
public void setUploadSize(long contentLength) { | |||
this.contentLength = contentLength; | |||
} | |||
/** | |||
* Go into upload state. This is to prevent double uploading on same | |||
* component. | |||
@@ -959,6 +828,8 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
private void endUpload() { | |||
isUploading = false; | |||
contentLength = -1; | |||
interrupted = false; | |||
requestRepaint(); | |||
} | |||
public boolean isUploading() { | |||
@@ -1046,4 +917,54 @@ public class Upload extends AbstractComponent implements Component.Focusable { | |||
this.buttonCaption = buttonCaption; | |||
} | |||
/* | |||
* Handle to terminal via Upload monitors and controls the upload during it | |||
* is being streamed. | |||
*/ | |||
private final ReceivingController controller = new ReceivingController() { | |||
public boolean listenProgress() { | |||
return (progressListeners != null || progressListener != null); | |||
} | |||
public void onProgress(ReceivingProgressedEvent event) { | |||
fireUpdateProgress(event.getBytesReceived(), | |||
event.getContentLength()); | |||
} | |||
public void uploadStarted(ReceivingStartedEvent event) { | |||
startUpload(); | |||
contentLength = event.getContentLength(); | |||
fireStarted(event.getFileName(), event.getMimeType()); | |||
} | |||
public void uploadFinished(ReceivingEndedEvent event) { | |||
fireUploadSuccess(event.getFileName(), event.getMimeType(), | |||
event.getContentLength()); | |||
endUpload(); | |||
requestRepaint(); | |||
} | |||
public void uploadFailed(ReceivingFailedEvent event) { | |||
Exception exception = event.getException(); | |||
if (exception instanceof NoInputStreamException) { | |||
fireNoInputStream(event.getFileName(), event.getMimeType(), 0); | |||
} else if (exception instanceof NoOutputStreamException) { | |||
fireNoOutputStream(event.getFileName(), event.getMimeType(), 0); | |||
} else { | |||
fireUploadInterrupted(event.getFileName(), event.getMimeType(), | |||
0, exception); | |||
} | |||
endUpload(); | |||
} | |||
public boolean isInterrupted() { | |||
return interrupted; | |||
} | |||
}; | |||
public final ReceivingController getReceivingController( | |||
com.vaadin.terminal.Receiver receiver) { | |||
return controller; | |||
} | |||
} |
@@ -38,8 +38,8 @@ import com.vaadin.ui.AbsoluteLayout.ComponentPosition; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.CssLayout; | |||
import com.vaadin.ui.DragAndDropWrapper; | |||
import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; | |||
import com.vaadin.ui.Embedded; | |||
import com.vaadin.ui.Html5File; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.SplitPanel; | |||
import com.vaadin.ui.Table; |
@@ -0,0 +1,129 @@ | |||
package com.vaadin.tests.dd; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.OutputStream; | |||
import org.apache.commons.io.output.NullOutputStream; | |||
import com.vaadin.event.dd.DragAndDropEvent; | |||
import com.vaadin.event.dd.DropHandler; | |||
import com.vaadin.event.dd.acceptcriteria.AcceptAll; | |||
import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; | |||
import com.vaadin.terminal.Receiver; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingEndedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingFailedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingProgressedEvent; | |||
import com.vaadin.terminal.ReceiverOwner.ReceivingStartedEvent; | |||
import com.vaadin.tests.components.TestBase; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.CssLayout; | |||
import com.vaadin.ui.DragAndDropWrapper; | |||
import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable; | |||
import com.vaadin.ui.Html5File; | |||
import com.vaadin.ui.Html5File.Html5FileUploadListener; | |||
import com.vaadin.ui.Label; | |||
public class DragAndDropFiles extends TestBase { | |||
@Override | |||
protected void setup() { | |||
CssLayout cssLayout = new CssLayout() { | |||
@Override | |||
protected String getCss(Component c) { | |||
return "display: block; padding:20px; border: 2px dotted black; background: #aaa;"; | |||
} | |||
}; | |||
Component l = new Label("Drag file on me"); | |||
l.setSizeUndefined(); | |||
cssLayout.addComponent(l); | |||
DragAndDropWrapper dragAndDropWrapper = new DragAndDropWrapper( | |||
cssLayout); | |||
dragAndDropWrapper.setSizeUndefined(); | |||
dragAndDropWrapper.setDropHandler(new DropHandler() { | |||
public AcceptCriterion getAcceptCriterion() { | |||
return AcceptAll.get(); | |||
} | |||
public void drop(DragAndDropEvent event) { | |||
WrapperTransferable transferable = (WrapperTransferable) event | |||
.getTransferable(); | |||
Html5File[] files = transferable.getFiles(); | |||
if (files != null) { | |||
for (int i = 0; i < files.length; i++) { | |||
Html5File file = files[i]; | |||
// Max 1 MB files are uploaded | |||
if (file.getFileSize() > 1024 * 1024) { | |||
getMainWindow() | |||
.showNotification( | |||
"File " | |||
+ file.getFileName() | |||
+ " was too large, not transferred to the server side."); | |||
continue; | |||
} | |||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |||
Receiver receiver = new Receiver() { | |||
public OutputStream receiveUpload(String filename, | |||
String MIMEType) { | |||
System.err.println("receiveUpload " + filename); | |||
return new NullOutputStream(); | |||
} | |||
}; | |||
file.setReceiver(receiver); | |||
Html5FileUploadListener listener = new Html5FileUploadListener() { | |||
public void uploadStarted( | |||
ReceivingStartedEvent event) { | |||
getMainWindow().showNotification( | |||
"Started uploading " | |||
+ event.getFileName()); | |||
} | |||
public void uploadFinished(ReceivingEndedEvent event) { | |||
getMainWindow().showNotification( | |||
"Finished uploading " | |||
+ event.getFileName()); | |||
} | |||
public void uploadFailed(ReceivingFailedEvent event) { | |||
getMainWindow().showNotification( | |||
"Failed uploading " | |||
+ event.getFileName()); | |||
} | |||
public void onProgress( | |||
ReceivingProgressedEvent event) { | |||
System.err.println("Progress" | |||
+ event.getBytesReceived()); | |||
} | |||
}; | |||
file.setUploadListener(listener); | |||
} | |||
} | |||
} | |||
}); | |||
addComponent(dragAndDropWrapper); | |||
} | |||
/* | |||
* TODO implement 'handbrake' for testing, progresss listener, interrupting. | |||
*/ | |||
@Override | |||
protected String getDescription() { | |||
return "Should work. Over 1 MB files will not be posted. TODO implement 'handbrake' for testing, progresss listener, interrupting."; | |||
} | |||
@Override | |||
protected Integer getTicketNumber() { | |||
return null; | |||
} | |||
} |
@@ -15,7 +15,7 @@ import com.vaadin.ui.AbsoluteLayout; | |||
import com.vaadin.ui.AbsoluteLayout.ComponentPosition; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.DragAndDropWrapper; | |||
import com.vaadin.ui.DragAndDropWrapper.WrapperTransferable.Html5File; | |||
import com.vaadin.ui.Html5File; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.Upload.Receiver; | |||