001/* 002 * (C) Copyright 2006-2008 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * bstefanescu 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.webengine.forms; 023 024import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 025import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; 026 027import java.io.File; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.UnsupportedEncodingException; 031import java.nio.file.Files; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037 038import javax.servlet.http.HttpServletRequest; 039 040import org.apache.commons.fileupload.FileItem; 041import org.apache.commons.fileupload.FileUploadException; 042import org.apache.commons.fileupload.RequestContext; 043import org.apache.commons.fileupload.disk.DiskFileItem; 044import org.apache.commons.fileupload.disk.DiskFileItemFactory; 045import org.apache.commons.fileupload.servlet.ServletFileUpload; 046import org.apache.commons.fileupload.servlet.ServletRequestContext; 047import org.apache.commons.lang3.StringUtils; 048import org.nuxeo.ecm.core.api.Blob; 049import org.nuxeo.ecm.core.api.Blobs; 050import org.nuxeo.ecm.core.api.DocumentModel; 051import org.nuxeo.ecm.core.api.NuxeoException; 052import org.nuxeo.ecm.core.api.PropertyException; 053import org.nuxeo.ecm.core.api.VersioningOption; 054import org.nuxeo.ecm.core.api.model.Property; 055import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 056import org.nuxeo.ecm.core.schema.types.ListType; 057import org.nuxeo.ecm.core.schema.types.Type; 058import org.nuxeo.ecm.webengine.forms.validation.Form; 059import org.nuxeo.ecm.webengine.forms.validation.FormManager; 060import org.nuxeo.ecm.webengine.forms.validation.ValidationException; 061import org.nuxeo.ecm.webengine.servlet.WebConst; 062 063/** 064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 065 */ 066public class FormData implements FormInstance { 067 068 public static final String PROPERTY = "property"; 069 070 public static final String TITLE = "dc:title"; 071 072 public static final String DOCTYPE = "doctype"; 073 074 public static final String VERSIONING = "versioning"; 075 076 public static final String MAJOR = "major"; 077 078 public static final String MINOR = "minor"; 079 080 protected static ServletFileUpload fu = new ServletFileUpload(new DiskFileItemFactory()); 081 082 protected final HttpServletRequest request; 083 084 protected boolean isMultipart = false; 085 086 protected RequestContext ctx; 087 088 // Multipart items cache 089 protected Map<String, List<FileItem>> items; 090 091 // parameter map cache - used in Multipart forms to convert to 092 // ServletRequest#getParameterMap 093 // format 094 // protected Map<String, String[]> parameterMap; 095 096 public FormData(HttpServletRequest request) { 097 this.request = request; 098 isMultipart = getIsMultipartContent(); 099 if (isMultipart) { 100 ctx = new ServletRequestContext(request); 101 } 102 } 103 104 protected String getString(FileItem item) { 105 try { 106 String enc = request.getCharacterEncoding(); 107 if (enc != null) { 108 return item.getString(request.getCharacterEncoding()); 109 } else { 110 return item.getString(); 111 } 112 } catch (UnsupportedEncodingException e) { 113 return item.getString(); 114 } 115 } 116 117 protected boolean getIsMultipartContent() { 118 String method = request.getMethod().toLowerCase(); 119 if (!"post".equals(method) && !"put".equals(method)) { 120 return false; 121 } 122 String contentType = request.getContentType(); 123 if (contentType == null) { 124 return false; 125 } 126 return contentType.toLowerCase().startsWith(WebConst.MULTIPART); 127 } 128 129 public boolean isMultipartContent() { 130 return isMultipart; 131 } 132 133 @Override 134 @SuppressWarnings("unchecked") 135 public Map<String, String[]> getFormFields() { 136 if (isMultipart) { 137 return getMultiPartFormFields(); 138 } else { 139 return request.getParameterMap(); 140 } 141 } 142 143 public Map<String, String[]> getMultiPartFormFields() { 144 Map<String, List<FileItem>> items = getMultiPartItems(); 145 Map<String, String[]> result = new HashMap<>(); 146 for (Map.Entry<String, List<FileItem>> entry : items.entrySet()) { 147 List<FileItem> list = entry.getValue(); 148 String[] ar = new String[list.size()]; 149 boolean hasEntries = false; 150 for (int i = 0; i < ar.length; i++) { 151 FileItem item = list.get(i); 152 if (item.isFormField()) { 153 ar[i] = getString(item); 154 hasEntries = true; 155 } 156 } 157 if (hasEntries) { 158 result.put(entry.getKey(), ar); 159 } 160 } 161 return result; 162 } 163 164 @SuppressWarnings("unchecked") 165 public Map<String, List<FileItem>> getMultiPartItems() { 166 if (items == null) { 167 if (!isMultipart) { 168 throw new IllegalStateException("Not in a multi part form request"); 169 } 170 try { 171 items = new HashMap<>(); 172 ServletRequestContext ctx = new ServletRequestContext(request); 173 List<FileItem> fileItems = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(ctx); 174 for (FileItem item : fileItems) { 175 String key = item.getFieldName(); 176 List<FileItem> list = items.get(key); 177 if (list == null) { 178 list = new ArrayList<>(); 179 items.put(key, list); 180 } 181 list.add(item); 182 } 183 } catch (FileUploadException e) { 184 throw new NuxeoException("Failed to get uploaded files", e); 185 } 186 } 187 return items; 188 } 189 190 @Override 191 @SuppressWarnings("unchecked") 192 public Collection<String> getKeys() { 193 if (isMultipart) { 194 return getMultiPartItems().keySet(); 195 } else { 196 return request.getParameterMap().keySet(); 197 } 198 } 199 200 @Override 201 public Blob getBlob(String key) { 202 FileItem item = getFileItem(key); 203 return item == null ? null : getBlob(item); 204 } 205 206 @Override 207 public Blob[] getBlobs(String key) { 208 List<FileItem> list = getFileItems(key); 209 Blob[] ar = null; 210 if (list != null) { 211 ar = new Blob[list.size()]; 212 for (int i = 0, len = list.size(); i < len; i++) { 213 ar[i] = getBlob(list.get(i)); 214 } 215 } 216 return ar; 217 } 218 219 /** 220 * XXX TODO implement it 221 */ 222 @Override 223 public Map<String, Blob[]> getBlobFields() { 224 throw new UnsupportedOperationException("Not yet implemented"); 225 } 226 227 public Blob getFirstBlob() { 228 Map<String, List<FileItem>> items = getMultiPartItems(); 229 for (List<FileItem> list : items.values()) { 230 for (FileItem item : list) { 231 if (!item.isFormField()) { 232 return getBlob(item); 233 } 234 } 235 } 236 return null; 237 } 238 239 protected Blob getBlob(FileItem item) { 240 try { 241 Blob blob; 242 if (item.isInMemory()) { 243 blob = Blobs.createBlob(item.get()); 244 } else { 245 File file; 246 if (item instanceof DiskFileItem // 247 && (file = ((DiskFileItem) item).getStoreLocation()) != null) { 248 // move the file to a temporary blob we own 249 blob = Blobs.createBlobWithExtension(null); 250 Files.move(file.toPath(), blob.getFile().toPath(), REPLACE_EXISTING); 251 } else { 252 // if we couldn't get to the file, use the InputStream 253 try (InputStream in = item.getInputStream()) { 254 blob = Blobs.createBlob(in); 255 } 256 } 257 } 258 blob.setMimeType(defaultIfEmpty(item.getContentType(), "application/octet-stream")); 259 blob.setFilename(item.getName()); 260 return blob; 261 } catch (IOException e) { 262 throw new NuxeoException("Failed to get blob data", e); 263 } 264 } 265 266 public final FileItem getFileItem(String key) { 267 Map<String, List<FileItem>> items = getMultiPartItems(); 268 List<FileItem> list = items.get(key); 269 if (list != null && !list.isEmpty()) { 270 return list.get(0); 271 } 272 return null; 273 } 274 275 public final List<FileItem> getFileItems(String key) { 276 return getMultiPartItems().get(key); 277 } 278 279 public String getMultiPartFormProperty(String key) { 280 FileItem item = getFileItem(key); 281 return item == null ? null : getString(item); 282 } 283 284 public String[] getMultiPartFormListProperty(String key) { 285 List<FileItem> list = getFileItems(key); 286 String[] ar = null; 287 if (list != null) { 288 ar = new String[list.size()]; 289 for (int i = 0, len = list.size(); i < len; i++) { 290 ar[i] = getString(list.get(i)); 291 } 292 } 293 return ar; 294 } 295 296 /** 297 * @return an array of strings or an array of blobs 298 */ 299 public Object[] getMultiPartFormItems(String key) { 300 return getMultiPartFormItems(getFileItems(key)); 301 } 302 303 public Object[] getMultiPartFormItems(List<FileItem> list) { 304 Object[] ar = null; 305 if (list != null) { 306 if (list.isEmpty()) { 307 return null; 308 } 309 FileItem item0 = list.get(0); 310 if (item0.isFormField()) { 311 ar = new String[list.size()]; 312 ar[0] = getString(item0); 313 for (int i = 1, len = list.size(); i < len; i++) { 314 ar[i] = getString(list.get(i)); 315 } 316 } else { 317 List<Blob> blobs = new ArrayList<>(); 318 for (FileItem item : list) { 319 if (!StringUtils.isBlank(item.getName())) { 320 blobs.add(getBlob(item)); 321 } 322 } 323 ar = blobs.toArray(new Blob[blobs.size()]); 324 } 325 } 326 return ar; 327 } 328 329 public final Object getFileItemValue(FileItem item) { 330 if (item.isFormField()) { 331 return getString(item); 332 } else { 333 return getBlob(item); 334 } 335 } 336 337 public String getFormProperty(String key) { 338 String[] value = request.getParameterValues(key); 339 if (value != null && value.length > 0) { 340 return value[0]; 341 } 342 return null; 343 } 344 345 public String[] getFormListProperty(String key) { 346 return request.getParameterValues(key); 347 } 348 349 @Override 350 public String getString(String key) { 351 if (isMultipart) { 352 return getMultiPartFormProperty(key); 353 } else { 354 return getFormProperty(key); 355 } 356 } 357 358 @Override 359 public String[] getList(String key) { 360 if (isMultipart) { 361 return getMultiPartFormListProperty(key); 362 } else { 363 return getFormListProperty(key); 364 } 365 } 366 367 @Override 368 public Object[] get(String key) { 369 if (isMultipart) { 370 return getMultiPartFormItems(key); 371 } else { 372 return getFormListProperty(key); 373 } 374 } 375 376 @Override 377 public void fillDocument(DocumentModel doc) { 378 try { 379 if (isMultipart) { 380 fillDocumentFromMultiPartForm(doc); 381 } else { 382 fillDocumentFromForm(doc); 383 } 384 } catch (PropertyException e) { 385 e.addInfo("Failed to fill document properties from request properties"); 386 throw e; 387 } 388 } 389 390 @SuppressWarnings("unchecked") 391 public void fillDocumentFromForm(DocumentModel doc) throws PropertyException { 392 Map<String, String[]> map = request.getParameterMap(); 393 for (Map.Entry<String, String[]> entry : map.entrySet()) { 394 String key = entry.getKey(); 395 if (key.indexOf(':') > -1) { // an XPATH property 396 Property p; 397 try { 398 p = doc.getProperty(key); 399 } catch (PropertyException e) { 400 continue; // not a valid property 401 } 402 String[] ar = entry.getValue(); 403 fillDocumentProperty(p, key, ar); 404 } 405 } 406 } 407 408 public void fillDocumentFromMultiPartForm(DocumentModel doc) throws PropertyException { 409 Map<String, List<FileItem>> map = getMultiPartItems(); 410 for (Map.Entry<String, List<FileItem>> entry : map.entrySet()) { 411 String key = entry.getKey(); 412 if (key.indexOf(':') > -1) { // an XPATH property 413 Property p; 414 try { 415 p = doc.getProperty(key); 416 } catch (PropertyException e) { 417 continue; // not a valid property 418 } 419 List<FileItem> list = entry.getValue(); 420 if (list.isEmpty()) { 421 fillDocumentProperty(p, key, null); 422 } else { 423 Object[] ar = getMultiPartFormItems(list); 424 fillDocumentProperty(p, key, ar); 425 } 426 } 427 } 428 } 429 430 static void fillDocumentProperty(Property p, String key, Object[] ar) throws PropertyException { 431 if (ar == null || ar.length == 0) { 432 p.remove(); 433 } else if (p.isScalar()) { 434 p.setValue(ar[0]); 435 } else if (p.isList()) { 436 if (!p.isContainer()) { // an array 437 p.setValue(ar); 438 } else { 439 Type elType = ((ListType) p.getType()).getFieldType(); 440 if (elType.isSimpleType()) { 441 p.setValue(ar); 442 } else if ("content".equals(elType.getName())) { 443 // list of blobs 444 List<Blob> blobs = new ArrayList<>(); 445 if (ar.getClass().getComponentType() == String.class) { // transform 446 // strings 447 // to 448 // blobs 449 for (Object obj : ar) { 450 blobs.add(Blobs.createBlob(obj.toString())); 451 } 452 } else { 453 for (Object obj : ar) { 454 blobs.add((Blob) obj); 455 } 456 } 457 p.setValue(blobs); 458 } else { 459 // complex properties will be ignored 460 // throw new 461 // WebException("Cannot create complex lists properties from HTML forms"); 462 } 463 } 464 } else if (p.isComplex()) { 465 if (p.getClass() == BlobProperty.class) { 466 // should be a file upload 467 Blob blob = null; 468 if (ar[0].getClass() == String.class) { 469 blob = Blobs.createBlob(ar[0].toString()); 470 } else { 471 blob = (Blob) ar[0]; 472 } 473 p.setValue(blob); 474 } else { 475 // complex properties will be ignored 476 // throw new WebException( 477 // "Cannot set complex properties from HTML forms. You need to set each sub-scalar property 478 // explicitely"); 479 } 480 } 481 } 482 483 public VersioningOption getVersioningOption() { 484 String val = getString(VERSIONING); 485 if (val != null) { 486 return val.equals(MAJOR) ? VersioningOption.MAJOR : val.equals(MINOR) ? VersioningOption.MINOR : null; 487 } 488 return null; 489 } 490 491 public String getDocumentType() { 492 return getString(DOCTYPE); 493 } 494 495 public String getDocumentTitle() { 496 return getString(TITLE); 497 } 498 499 public <T extends Form> T validate(Class<T> type) throws ValidationException { 500 T proxy = FormManager.newProxy(type); 501 try { 502 proxy.load(this, proxy); 503 return proxy; 504 } catch (ValidationException e) { 505 e.setForm(proxy); 506 throw e; 507 } 508 } 509 510}