Гобелен имеет отличную поддержку JavaScript и Ajax. Он обеспечивает идеальный баланс между тем, насколько должен помочь фреймворк, и тем, что должен сделать разработчик. Большинство основанных на компонентах сред, а не вспомогательных, контролирующих, и большинство основанных на действиях сред оставляют даже интеграцию для разработчика. Tapestry предоставляет вам события, которые вы можете легко подключить к вашим событиям / функциям JavaScript, а все остальное, как я постоянно говорю, волшебство.
В этой статье я расскажу об интеграции библиотеки загрузки на основе Ajax, загрузчика файлов с Tapestry.
Ajax Upload
Загрузка на основе Ajax поддерживается не всеми браузерами. Таким образом, эта библиотека возвращается к отправке формы на основе iframe, если браузер не поддерживает загрузку Ajax. Если вы хотите узнать больше об Ajax-upload, вы можете прочитать очень хорошо написанный исходный код этой библиотеки вместе с этим .
Хотя эта библиотека очень хорошая, но в ней отсутствуют некоторые функции, которые я должен был добавить сам.
- После загрузки вложения удалить его невозможно.
- Если форма должна быть отправлена повторно, также должны отображаться предыдущие загруженные файлы.
- Текст, отображаемый на кнопках, не настраивается
С таким хорошо написанным сценарием это было довольно легко сделать. Я не собираюсь обсуждать модификации за исключением тех, которые имеют непосредственное отношение к интеграции.
Расшифровка запроса
Запрос на загрузку может быть представлен в двух формах: в виде Ajax-запроса с входным потоком в качестве загруженного файла или в виде составного запроса. О последнем уже позаботился модуль MultipartServletRequestFilter модуля загрузки гобеленов, который проверяет, является ли запрос многокомпонентным, и, если он есть, затем декодирует его, используя MultipartDecoder. Декодер использует библиотеку загрузки Apache commons для декодирования запроса и сохраняет загруженные файлы в виде UploadedFile. Единственное, что заставило меня переопределить службу MultipartDecoder, — это очистка, выполняемая в конце запроса. Поскольку при загрузке Ajax будет использоваться несколько запросов (по одному на каждую загрузку и один на отправку формы), очистка должна быть предотвращена, поэтому в классе Module мы переопределяем службу
@Scope(ScopeConstants.PERTHREAD) public static MultipartDecoder buildMultipartDecoder2(RegistryShutdownHub shutdownHub, @Autobuild MultipartDecoderImpl multipartDecoder) { if (needToAddShutdownListener.getAndSet(false)) { shutdownHub.addRegistryShutdownListener(new RegistryShutdownListener() { @Override public void registryDidShutdown() { FileCleaner.exitWhenFinished(); } }); } return multipartDecoder; } public static void contributeServiceOverride(@InjectService("MultipartDecoder2") MultipartDecoder multipartDecoder, @SuppressWarnings("rawtypes") MappedConfiguration<Class, Object> overrides) { overrides.add(MultipartDecoder.class, multipartDecoder); }
Запрос на загрузку Ajax декодируется аналогичным образом. Существует AjaxUploadServletRequestFilter, который проверяет, является ли запрос запросом загрузки Ajax, и в случае, если он затем передает запрос AjaxUploadDecoder для извлечения загруженного файла.
public class AjaxUploadServletRequestFilter implements HttpServletRequestFilter { private AjaxUploadDecoder decoder; public AjaxUploadServletRequestFilter(AjaxUploadDecoder decoder) { this.decoder = decoder; } @Override public boolean service(HttpServletRequest request, HttpServletResponse response, HttpServletRequestHandler handler) throws IOException { if (decoder.isAjaxUploadRequest(request)) { decoder.setupUploadedFile(request); } return handler.service(request, response); } }
Интерфейс и реализация загрузочного декодера Ajax ниже
public interface AjaxUploadDecoder { boolean isAjaxUploadRequest(HttpServletRequest request); boolean isAjaxUploadRequest(Request request); UploadedFile getFileUpload(); void setupUploadedFile(HttpServletRequest request); } public class AjaxUploadDecoderImpl implements AjaxUploadDecoder { private UploadedFileItem uploadedFile; public static final String AJAX_UPLOAD_HEADER = "X-File-Name"; private FileItemFactory fileItemFactory; public AjaxUploadDecoderImpl(FileItemFactory fileItemFactory) { this.fileItemFactory = fileItemFactory; } @Override public boolean isAjaxUploadRequest(HttpServletRequest request) { return request.getHeader(AJAX_UPLOAD_HEADER) != null; } @Override public boolean isAjaxUploadRequest(Request request) { return request.isXHR() && request.getHeader(AJAX_UPLOAD_HEADER) != null; } @Override public void setupUploadedFile(HttpServletRequest request) { String fieldName = request.getHeader(AJAX_UPLOAD_HEADER); FileItem item = fileItemFactory.createItem(fieldName, request.getContentType(), false, request.getParameter(AjaxUploadConstants.FILE_PARAMETER)); try { TapestryInternalUtils.copy(request.getInputStream(), item.getOutputStream()); } catch (IOException e) { throw new RuntimeException("Could not copy request's input stream to file", e); } uploadedFile = new UploadedFileItem(item); } @Override public UploadedFile getFileUpload() { return uploadedFile; } }
Основная задача создания загрузки делегирована в FileItemFactory библиотеки загрузки Apache Commons.
интеграция
Скрипт загрузки требует следующих аргументов:
- element: элемент, который будет использоваться для создания компонента
- action: URL, на который будет отправлен файл. Скрипт ожидает JSON в respose с success = true для успешной загрузки. В случае ошибки, поле ошибки должно содержать сообщение об ошибке
- cancelLink: URL, который будет вызываться при отмене загрузки
- removeLink: URL, который будет вызываться при удалении загруженного файла.
- initializeUploadsLink: URL, который будет вызываться при загрузке компонента. Сценарий ожидает JSON, содержащий список изначально загруженных файлов, в случае повторной отправки
- sizeLimit: максимальный размер файла, который может быть загружен
- name: имя созданного поля файла
- uploadText: текст для отображения на кнопке загрузки
- dropText: текст для отображения в области перетаскивания в случае перетаскивания
Все это делает компонент AjaxUpload
@SupportsInformalParameters @Import(library = "ajaxupload.js", stylesheet = "ajaxupload.css") public class AjaxUpload extends AbstractField { public static final String FILE_PARAMETER = "qqfile"; private static final String STYLE_TO_HIDE_INPUT_TEXT = "display:inline;" + "color:transparent;background:transparent;" + "border:0;height:1px;width:1px;"; @Inject private JavaScriptSupport javaScriptSupport; @Inject private ComponentResources resources; @Parameter(required = true, autoconnect = true, principal = true) private List<UploadedFile> value; @SuppressWarnings("unused") @Parameter private boolean uploaded; @Symbol(UploadSymbols.REQUESTSIZE_MAX) @Inject private int maxSize; @Parameter(value = "1", defaultPrefix = BindingConstants.LITERAL) private int maxFiles; @Inject private Request request; @Inject private Messages messages; @Inject private MultipartDecoder multipartDecoder; @Inject private AjaxUploadDecoder ajaxDecoder; /** * The object that will perform input validation. The "validate:" binding * prefix is generally used to provide this object in a declarative fashion. */ @Parameter(defaultPrefix = BindingConstants.VALIDATE) private FieldValidator<Object> validate; @Environmental private ValidationTracker tracker; @SuppressWarnings("unused") @Environmental private FormSupport formSupport; @Inject private ComponentDefaultProvider defaultProvider; @Inject private FieldValidationSupport fieldValidationSupport; @SuppressWarnings("unused") @Mixin private RenderDisabled renderDisabled; /** * Computes a default value for the "validate" parameter using * {@link FieldValidatorDefaultSource}. */ final Binding defaultValidate() { return defaultProvider.defaultValidatorBinding("value", resources); } public AjaxUpload() { } // For testing AjaxUpload(List<UploadedFile> value, FieldValidator<Object> validate, MultipartDecoder multipartDecoder, AjaxUploadDecoder ajaxDecoder, ValidationTracker tracker, ComponentResources resources, FieldValidationSupport fieldValidationSupport, JavaScriptSupport javaScriptSupport) { this.value = value; if (validate != null) this.validate = validate; this.multipartDecoder = multipartDecoder; this.tracker = tracker; this.resources = resources; this.fieldValidationSupport = fieldValidationSupport; this.javaScriptSupport = javaScriptSupport; this.ajaxDecoder = ajaxDecoder; maxFiles = 1; } void beginRender(MarkupWriter writer) { writer.element("input", "type", "text", "id", getClientId(), "style", STYLE_TO_HIDE_INPUT_TEXT, "name", getControlName()); validate.render(writer); decorateInsideField(); } private String getWrapperClientId() { return getClientId() + "_wrapper"; } private String getFileControlName() { return getControlName() + "_file"; } void afterRender(final MarkupWriter writer) { writer.end(); writer.element("span", "id", getWrapperClientId(), "style", "display:inline-block"); writer.end(); } @AfterRender void addJavaScript() { JSONObject arguments = fillArguments(); javaScriptSupport.addScript("new qq.FileUploader(%s);", arguments); } JSONObject fillArguments() { JSONObject spec = new JSONObject(); for (String informalParameter : resources.getInformalParameterNames()) { spec.put(informalParameter, resources.getInformalParameter(informalParameter, String.class)); } spec.put("element", getElementId()); spec.put("sizeField", getControlName()); spec.put("uploadText", messages.get("ajaxupload.upload-text")); spec.put("dropText", messages.get("ajaxupload.drop-text")); spec.put("action", getUploadLink()); spec.put("cancelLink", getCancelLink()); spec.put("removeLink", getRemoveLink()); spec.put("initializeUploadsLink", getInitializeUploadsLink()); spec.put("sizeLimit", maxSize < 0 ? 0 : maxSize); spec.put("name", getFileControlName()); spec.put("id", getFileControlName()); return spec; } private Object getElementId() { return new JSONLiteral("document.getElementById('" + getWrapperClientId() + "')"); } String getUploadLink() { final Link link = resources.createEventLink("upload"); return link.toAbsoluteURI(); } String getCancelLink() { final Link cancelLink = resources.createEventLink("cancelUpload"); return cancelLink.toAbsoluteURI(); } String getRemoveLink() { final Link removeLink = resources.createEventLink("removeUpload"); return removeLink.toAbsoluteURI(); } String getInitializeUploadsLink() { final Link initializeUploadsLink = resources.createEventLink("initializeUploads"); return initializeUploadsLink.toAbsoluteURI(); } /** * Converts the current list of uploaded files to a JSON object containing a json array * with each element containing the name of the file and a unique key for * identification. The unique key is index of the uploaded file in parameter * <code>value</code> * * @return */ JSONObject onInitializeUploads() { JSONArray array = new JSONArray(); if (value != null) { for (int i = 0; i < value.size(); ++i) { if (value.get(i) == null) { continue; } JSONObject indexWithFileName = new JSONObject(); indexWithFileName.put("serverIndex", i); indexWithFileName.put("fileName", value.get(i).getFilePath()); array.put(indexWithFileName); } } return new JSONObject().put("uploads", array); } Object onUpload() { if (hasMaximumFileUploadCountReached()) { return createFailureResponse(messages.format("ajaxupload.maxfiles", maxFiles)); } final UploadedFile uploadedFile; if (isAjaxUpload()) { uploadedFile = createUploadedFileFromRequestInputStream(); } else { uploadedFile = createUploadedFileFromMultipartForm(); } if (value == null) { value = new ArrayList<UploadedFile>(); } value.add(uploadedFile); return createSuccessResponse(value.size() - 1); // Last index } private boolean hasMaximumFileUploadCountReached() { if (value == null) { return maxFiles <= 0; } // Can't rely on value's size as some of the values can be null int size = 0; for (UploadedFile uploadedFile : value) { if (uploadedFile != null) { size++; } } return this.maxFiles <= size; } private boolean isAjaxUpload() { return ajaxDecoder.isAjaxUploadRequest(request); } private UploadedFile createUploadedFileFromMultipartForm() { return multipartDecoder.getFileUpload(FILE_PARAMETER); } private UploadedFile createUploadedFileFromRequestInputStream() { return ajaxDecoder.getFileUpload(); } private Object createFailureResponse(String errorMessage) { JSONObject response = new JSONObject(); response.put("success", false); response.put("error", errorMessage); if (!request.isXHR()) { return new StatusResponse(response.toString()); } else { return response; } } private Object createSuccessResponse(int serverIndex) { JSONObject response = new JSONObject(); response.put("success", true); response.put("serverIndex", serverIndex); if (!request.isXHR()) { return new StatusResponse(response.toString()); } else { return response; } } void onRemoveUpload(int serverIndex) { // We use index of an uploadedFile in 'value' as a key at // the client side and if the uploaded file is removed we cleanup and set // the element at that index to null. As the 'value' may contain null, we // need to remove those entries in processSubmission() if (value != null && serverIndex >= 0 && serverIndex < value.size()) { UploadedFile item = value.get(serverIndex); if (item != null && (item instanceof UploadedFileItem)) { ((UploadedFileItem) item).cleanup(); } value.set(serverIndex, null); } } void onCancelUpload(String fileName) { // TODO: Some how remove the partially uploaded file } @Override protected void processSubmission(String elementName) { // Nothing to process from current request as the uploads have already // been received and stored in value if (value != null) { // Remove any null values in 'value' removeNullsFromValue(); } try { fieldValidationSupport.validate(value, resources, validate); } catch (ValidationException ex) { tracker.recordError(this, ex.getMessage()); } } private void removeNullsFromValue() { List<UploadedFile> uploads = new ArrayList<UploadedFile>(); for (UploadedFile upload : value) { if (upload != null) { uploads.add(upload); } } value = uploads; } public List<UploadedFile> getValue() { return value; } @Override public boolean isRequired() { return value != null && value.size() > 0; } AjaxUpload injectDecorator(ValidationDecorator decorator) { setDecorator(decorator); return this; } AjaxUpload injectRequest(Request request) { this.request = request; return this; } AjaxUpload injectFormSupport(FormSupport formSupport) { // We have our copy ... this.formSupport = formSupport; // As does AbstractField setFormSupport(formSupport); return this; } AjaxUpload injectFieldValidator(FieldValidator<Object> validator) { this.validate = validator; return this; } void injectResources(ComponentResources resources) { this.resources = resources; } void injectValue(List<UploadedFile> value) { this.value = value; } void injectMessages(Messages messages) { this.messages = messages; } static class StatusResponse implements StreamResponse { private String text; StatusResponse(String text) { this.text = text; } @Override public String getContentType() { return "text/html"; } @Override public InputStream getStream() throws IOException { return new ByteArrayInputStream(text.toString().getBytes()); } @Override public void prepareResponse(Response response) { } public JSONObject getJSON() { return new JSONObject(text); } } public void injectFieldValidationSupport(FieldValidationSupport support) { this.fieldValidationSupport = support; } }
Обратите внимание, я встроил текстовое поле в компонент. Причиной этого является облегчение поддержки валидации на стороне клиента, которая требует ввода (поскольку он использует атрибут формы компонента, скрытый использовать нельзя, так как он не виден).
В запросе на загрузку мы проверяем тип запроса, а затем получаем загруженный файл от соответствующего декодера. Мы отправляем индекс загруженного файла в скрипт, который используется для однозначной идентификации загруженного файла. Когда файл должен быть удален, этот индекс отправляется с запросом и на основе этого индекса файл удаляется.
использование
Может использоваться в шаблоне как
<form t:type='form'> <input t:type='tawus/ajaxupload' t:id='uploads'/> </form>
и в файле класса
@Persist @Property private List<UploadedFile> uploads; void onSuccess(){ //Use uploads uploads.clear(); }
Значение должно быть сохранено на значение должно жить несколько запросов.
Вы можете найти модуль здесь
От http://tawus.wordpress.com/2011/06/25/ajax-upload-for-tapestry/