Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions stroom-app/src/main/resources/ui/css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
height: 100%;
}

.ace_gutter-cell {
color: var(--text-color);
}

.editor .hl {
position: absolute;
/* Your styling options here are limited as the highlight is a separate layer to the text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@

package stroom.widget.form.client;

import stroom.task.client.TaskMonitorFactory;
import stroom.util.shared.ResourceKey;
import stroom.widget.button.client.Button;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeUri;
import com.google.gwt.safehtml.shared.annotations.IsSafeUri;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FileUpload;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteHandler;
import com.google.gwt.user.client.ui.FormPanel.SubmitHandler;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;

import java.util.function.Consumer;

public class CustomFileUpload extends Composite {

private static final Binder BINDER = GWT.create(Binder.class);
Expand All @@ -45,14 +45,13 @@ public class CustomFileUpload extends Composite {
@UiField
Label fileName;
@UiField
FormPanel form;
@UiField
FileUpload fileUpload;

private String actionUrl;
private final FileUploadResultHandler resultHandler = new FileUploadResultHandler();

private CustomFileUpload() {
widget = BINDER.createAndBindUi(this);
form.setEncoding(FormPanel.ENCODING_MULTIPART);
form.setMethod(FormPanel.METHOD_POST);
fileUpload.setVisible(false);
initWidget(widget);
}
Expand All @@ -62,44 +61,49 @@ public Widget asWidget() {
return widget;
}

public FormPanel getForm() {
return form;
}

public String getFilename() {
return fileUpload.getFilename();
}

public HandlerRegistration addSubmitCompleteHandler(final SubmitCompleteHandler handler) {
return form.addSubmitCompleteHandler(handler);
/**
* Set the consumer called with the {@link ResourceKey} of the uploaded file when an upload
* triggered by {@link #submit()} succeeds.
*/
public CustomFileUpload onSuccess(final Consumer<ResourceKey> successConsumer) {
resultHandler.onSuccess(successConsumer);
return this;
}

public HandlerRegistration addSubmitHandler(final SubmitHandler handler) {
return form.addSubmitHandler(handler);
/**
* Set the consumer called with an error message when an upload triggered by {@link #submit()}
* fails.
*/
public CustomFileUpload onFailure(final Consumer<String> failureConsumer) {
resultHandler.onFailure(failureConsumer);
return this;
}

public void reset() {
form.reset();
/**
* Set the task monitor factory used to indicate progress for the duration of the upload.
*/
public CustomFileUpload taskMonitorFactory(final TaskMonitorFactory taskMonitorFactory,
final String taskMessage) {
resultHandler.taskMonitorFactory(taskMonitorFactory, taskMessage);
return this;
}

public void setAction(@IsSafeUri final String url) {
form.setAction(url);
this.actionUrl = url;
}

public void setAction(final SafeUri url) {
form.setAction(url);
}

public void setEncoding(final String encodingType) {
form.setEncoding(encodingType);
}

public void setMethod(final String method) {
form.setMethod(method);
this.actionUrl = url.asString();
}

public void submit() {
form.submit();
// Upload via XMLHttpRequest rather than a native form submission so we can attach the
// X-CSRF header that the server requires for session-authenticated requests.
FileUploadSubmitter.submit(actionUrl, fileUpload.getElement(), resultHandler);
}

public void focus() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2016-2025 Crown Copyright
*
* 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 stroom.widget.form.client;

/**
* Callback for an asynchronous file upload performed by {@link FileUploadSubmitter}.
*/
interface FileUploadCallback {

/**
* Called just before the upload request is sent.
*/
void onUploadStart();

/**
* Called when the server responds with a success (2xx) status.
*
* @param responseText The raw response body returned by the upload servlet.
*/
void onUploadSuccess(String responseText);

/**
* Called when the upload could not be completed, e.g. a non-2xx response or a network error.
*
* @param message A human-readable description of the failure.
*/
void onUploadFailure(String message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2016-2025 Crown Copyright
*
* 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 stroom.widget.form.client;

import stroom.task.client.SimpleTask;
import stroom.task.client.Task;
import stroom.task.client.TaskMonitor;
import stroom.task.client.TaskMonitorFactory;
import stroom.util.shared.PropertyMap;
import stroom.util.shared.ResourceKey;

import java.util.function.Consumer;

/**
* Handles the result of a file upload: parses the server's response, drives an optional task
* monitor for the duration of the upload, and dispatches to the configured success/failure
* consumers. Configured fluently, mirroring the {@code RestFactory} builder.
*/
class FileUploadResultHandler implements FileUploadCallback {

private Consumer<ResourceKey> successConsumer = resourceKey -> {
};
private Consumer<String> failureConsumer = message -> {
};
private TaskMonitorFactory taskMonitorFactory;
private String taskMessage = "Uploading";

private Task task;
private TaskMonitor taskMonitor;

public FileUploadResultHandler onSuccess(final Consumer<ResourceKey> successConsumer) {
this.successConsumer = successConsumer;
return this;
}

public FileUploadResultHandler onFailure(final Consumer<String> failureConsumer) {
this.failureConsumer = failureConsumer;
return this;
}

public FileUploadResultHandler taskMonitorFactory(final TaskMonitorFactory taskMonitorFactory) {
this.taskMonitorFactory = taskMonitorFactory;
return this;
}

public FileUploadResultHandler taskMonitorFactory(final TaskMonitorFactory taskMonitorFactory,
final String taskMessage) {
this.taskMonitorFactory = taskMonitorFactory;
this.taskMessage = taskMessage;
return this;
}

@Override
public void onUploadStart() {
if (taskMonitorFactory != null) {
task = new SimpleTask(taskMessage);
taskMonitor = taskMonitorFactory.createTaskMonitor();
taskMonitor.onStart(task);
}
}

@Override
public void onUploadSuccess(final String result) {
try {
if (result != null) {
final PropertyMap propertyMap = new PropertyMap();
propertyMap.loadArgLine(result);

if (propertyMap.isSuccess()) {
successConsumer.accept(new ResourceKey(propertyMap));
} else {
failureConsumer.accept(propertyMap.get("exception"));
}
} else {
failureConsumer.accept("Unable to read file");
}
} catch (final RuntimeException e) {
failureConsumer.accept(e.getMessage());
} finally {
endTask();
}
}

@Override
public void onUploadFailure(final String message) {
try {
failureConsumer.accept(message);
} finally {
endTask();
}
}

private void endTask() {
if (taskMonitor != null) {
taskMonitor.onEnd(task);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2016-2025 Crown Copyright
*
* 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 stroom.widget.form.client;

import com.google.gwt.dom.client.Element;

/**
* Uploads the file selected in a native {@code <input type="file">} element via
* {@code XMLHttpRequest} using a multipart {@code FormData} body.
* <p>
* Unlike a native HTML form submission (as done by GWT's {@code FormPanel}), an
* {@code XMLHttpRequest} lets us attach the custom {@code X-CSRF} header that the server's
* security filter requires for session-authenticated, state-changing requests. Cross-site
* scripts cannot set custom headers, so the header proves the request came from our own UI.
*/
final class FileUploadSubmitter {

/**
* Must match {@code ImportFileServlet.FILE_UPLOAD_PROP_NAME} on the server.
*/
private static final String FILE_FIELD_NAME = "fileUpload";
private static final String CSRF_HEADER = "X-CSRF";
private static final String CSRF_VALUE = "1";

private FileUploadSubmitter() {
// Utility class.
}

/**
* Upload the file currently selected in the given file input element to the given URL.
*
* @param url The upload endpoint (same-origin as the app).
* @param fileInputElement The {@code <input type="file">} element holding the selected file.
* @param callback Notified of the start, success or failure of the upload.
*/
static void submit(final String url,
final Element fileInputElement,
final FileUploadCallback callback) {
callback.onUploadStart();
doSubmit(url, fileInputElement, FILE_FIELD_NAME, CSRF_HEADER, CSRF_VALUE, callback);
}

private static native void doSubmit(final String url,
final Element fileInput,
final String fieldName,
final String csrfHeader,
final String csrfValue,
final FileUploadCallback callback) /*-{
var files = fileInput.files;
if (!files || files.length === 0) {
callback.@stroom.widget.form.client.FileUploadCallback::onUploadFailure(Ljava/lang/String;)(
"No file selected");
return;
}

var formData = new FormData();
formData.append(fieldName, files[0]);

var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// Send the session cookie and attach the CSRF header the server requires.
xhr.withCredentials = true;
xhr.setRequestHeader(csrfHeader, csrfValue);

xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
callback.@stroom.widget.form.client.FileUploadCallback::onUploadSuccess(Ljava/lang/String;)(
xhr.responseText);
} else {
callback.@stroom.widget.form.client.FileUploadCallback::onUploadFailure(Ljava/lang/String;)(
"Upload failed (HTTP " + xhr.status + ")");
}
};
xhr.onerror = function () {
callback.@stroom.widget.form.client.FileUploadCallback::onUploadFailure(Ljava/lang/String;)(
"Network error during upload");
};

xhr.send(formData);
}-*/;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
<g:FlowPanel styleName="customFileUpload">
<b:Button ui:field="chooseFile" text="Choose File" addStyleNames="allow-focus Button--contained-primary"/>
<g:Label ui:field="fileName"/>
<g:FormPanel ui:field="form" styleName="max">
<g:FileUpload ui:field="fileUpload" addStyleNames="stroom-control wide-control" name="fileUpload"/>
</g:FormPanel>
<g:FileUpload ui:field="fileUpload" addStyleNames="stroom-control wide-control" name="fileUpload"/>
</g:FlowPanel>
</ui:UiBinder>
Loading
Loading