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    @SuppressWarnings("unchecked")
134    public Map<String, String[]> getFormFields() {
135        if (isMultipart) {
136            return getMultiPartFormFields();
137        } else {
138            return request.getParameterMap();
139        }
140    }
141
142    public Map<String, String[]> getMultiPartFormFields() {
143        Map<String, List<FileItem>> items = getMultiPartItems();
144        Map<String, String[]> result = new HashMap<String, String[]>();
145        for (Map.Entry<String, List<FileItem>> entry : items.entrySet()) {
146            List<FileItem> list = entry.getValue();
147            String[] ar = new String[list.size()];
148            for (int i = 0; i < ar.length; i++) {
149                ar[i] = getString(list.get(i));
150            }
151            result.put(entry.getKey(), ar);
152        }
153        return result;
154    }
155
156    @SuppressWarnings("unchecked")
157    public Map<String, List<FileItem>> getMultiPartItems() {
158        if (items == null) {
159            if (!isMultipart) {
160                throw new IllegalStateException("Not in a multi part form request");
161            }
162            try {
163                items = new HashMap<String, List<FileItem>>();
164                ServletRequestContext ctx = new ServletRequestContext(request);
165                List<FileItem> fileItems = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(ctx);
166                for (FileItem item : fileItems) {
167                    String key = item.getFieldName();
168                    List<FileItem> list = items.get(key);
169                    if (list == null) {
170                        list = new ArrayList<FileItem>();
171                        items.put(key, list);
172                    }
173                    list.add(item);
174                }
175            } catch (FileUploadException e) {
176                throw new NuxeoException("Failed to get uploaded files", e);
177            }
178        }
179        return items;
180    }
181
182    @SuppressWarnings("unchecked")
183    public Collection<String> getKeys() {
184        if (isMultipart) {
185            return getMultiPartItems().keySet();
186        } else {
187            return ((Map<String, String[]>) request.getParameterMap()).keySet();
188        }
189    }
190
191    public Blob getBlob(String key) {
192        FileItem item = getFileItem(key);
193        return item == null ? null : getBlob(item);
194    }
195
196    public Blob[] getBlobs(String key) {
197        List<FileItem> list = getFileItems(key);
198        Blob[] ar = null;
199        if (list != null) {
200            ar = new Blob[list.size()];
201            for (int i = 0, len = list.size(); i < len; i++) {
202                ar[i] = getBlob(list.get(i));
203            }
204        }
205        return ar;
206    }
207
208    /**
209     * XXX TODO implement it
210     */
211    public Map<String, Blob[]> getBlobFields() {
212        throw new UnsupportedOperationException("Not yet implemented");
213    }
214
215    public Blob getFirstBlob() {
216        Map<String, List<FileItem>> items = getMultiPartItems();
217        for (List<FileItem> list : items.values()) {
218            for (FileItem item : list) {
219                if (!item.isFormField()) {
220                    return getBlob(item);
221                }
222            }
223        }
224        return null;
225    }
226
227    protected Blob getBlob(FileItem item) {
228        try {
229            Blob blob;
230            if (item.isInMemory()) {
231                blob = Blobs.createBlob(item.get());
232            } else {
233                File file;
234                if (item instanceof DiskFileItem //
235                        && (file = ((DiskFileItem) item).getStoreLocation()) != null) {
236                    // move the file to a temporary blob we own
237                    blob = Blobs.createBlobWithExtension(null);
238                    Files.move(file.toPath(), blob.getFile().toPath(), REPLACE_EXISTING);
239                } else {
240                    // if we couldn't get to the file, use the InputStream
241                    try (InputStream in = item.getInputStream()) {
242                        blob = Blobs.createBlob(in);
243                    }
244                }
245            }
246            blob.setMimeType(defaultIfEmpty(item.getContentType(), "application/octet-stream"));
247            blob.setFilename(item.getName());
248            return blob;
249        } catch (IOException e) {
250            throw new NuxeoException("Failed to get blob data", e);
251        }
252    }
253
254    public final FileItem getFileItem(String key) {
255        Map<String, List<FileItem>> items = getMultiPartItems();
256        List<FileItem> list = items.get(key);
257        if (list != null && !list.isEmpty()) {
258            return list.get(0);
259        }
260        return null;
261    }
262
263    public final List<FileItem> getFileItems(String key) {
264        return getMultiPartItems().get(key);
265    }
266
267    public String getMultiPartFormProperty(String key) {
268        FileItem item = getFileItem(key);
269        return item == null ? null : getString(item);
270    }
271
272    public String[] getMultiPartFormListProperty(String key) {
273        List<FileItem> list = getFileItems(key);
274        String[] ar = null;
275        if (list != null) {
276            ar = new String[list.size()];
277            for (int i = 0, len = list.size(); i < len; i++) {
278                ar[i] = getString(list.get(i));
279            }
280        }
281        return ar;
282    }
283
284    /**
285     * @param key
286     * @return an array of strings or an array of blobs
287     */
288    public Object[] getMultiPartFormItems(String key) {
289        return getMultiPartFormItems(getFileItems(key));
290    }
291
292    public Object[] getMultiPartFormItems(List<FileItem> list) {
293        Object[] ar = null;
294        if (list != null) {
295            if (list.isEmpty()) {
296                return null;
297            }
298            FileItem item0 = list.get(0);
299            if (item0.isFormField()) {
300                ar = new String[list.size()];
301                ar[0] = getString(item0);
302                for (int i = 1, len = list.size(); i < len; i++) {
303                    ar[i] = getString(list.get(i));
304                }
305            } else {
306                List<Blob> blobs = new ArrayList<Blob>();
307                for (FileItem item : list) {
308                    if (!StringUtils.isBlank(item.getName())) {
309                        blobs.add(getBlob(item));
310                    }
311                }
312                ar = blobs.toArray(new Blob[blobs.size()]);
313            }
314        }
315        return ar;
316    }
317
318    public final Object getFileItemValue(FileItem item) {
319        if (item.isFormField()) {
320            return getString(item);
321        } else {
322            return getBlob(item);
323        }
324    }
325
326    public String getFormProperty(String key) {
327        String[] value = request.getParameterValues(key);
328        if (value != null && value.length > 0) {
329            return value[0];
330        }
331        return null;
332    }
333
334    public String[] getFormListProperty(String key) {
335        return request.getParameterValues(key);
336    }
337
338    public String getString(String key) {
339        if (isMultipart) {
340            return getMultiPartFormProperty(key);
341        } else {
342            return getFormProperty(key);
343        }
344    }
345
346    public String[] getList(String key) {
347        if (isMultipart) {
348            return getMultiPartFormListProperty(key);
349        } else {
350            return getFormListProperty(key);
351        }
352    }
353
354    public Object[] get(String key) {
355        if (isMultipart) {
356            return getMultiPartFormItems(key);
357        } else {
358            return getFormListProperty(key);
359        }
360    }
361
362    public void fillDocument(DocumentModel doc) {
363        try {
364            if (isMultipart) {
365                fillDocumentFromMultiPartForm(doc);
366            } else {
367                fillDocumentFromForm(doc);
368            }
369        } catch (PropertyException e) {
370            e.addInfo("Failed to fill document properties from request properties");
371            throw e;
372        }
373    }
374
375    @SuppressWarnings("unchecked")
376    public void fillDocumentFromForm(DocumentModel doc) throws PropertyException {
377        Map<String, String[]> map = request.getParameterMap();
378        for (Map.Entry<String, String[]> entry : map.entrySet()) {
379            String key = entry.getKey();
380            if (key.indexOf(':') > -1) { // an XPATH property
381                Property p;
382                try {
383                    p = doc.getProperty(key);
384                } catch (PropertyException e) {
385                    continue; // not a valid property
386                }
387                String[] ar = entry.getValue();
388                fillDocumentProperty(p, key, ar);
389            }
390        }
391    }
392
393    public void fillDocumentFromMultiPartForm(DocumentModel doc) throws PropertyException {
394        Map<String, List<FileItem>> map = getMultiPartItems();
395        for (Map.Entry<String, List<FileItem>> entry : map.entrySet()) {
396            String key = entry.getKey();
397            if (key.indexOf(':') > -1) { // an XPATH property
398                Property p;
399                try {
400                    p = doc.getProperty(key);
401                } catch (PropertyException e) {
402                    continue; // not a valid property
403                }
404                List<FileItem> list = entry.getValue();
405                if (list.isEmpty()) {
406                    fillDocumentProperty(p, key, null);
407                } else {
408                    Object[] ar = getMultiPartFormItems(list);
409                    fillDocumentProperty(p, key, ar);
410                }
411            }
412        }
413    }
414
415    static void fillDocumentProperty(Property p, String key, Object[] ar) throws PropertyException {
416        if (ar == null || ar.length == 0) {
417            p.remove();
418        } else if (p.isScalar()) {
419            p.setValue(ar[0]);
420        } else if (p.isList()) {
421            if (!p.isContainer()) { // an array
422                p.setValue(ar);
423            } else {
424                Type elType = ((ListType) p.getType()).getFieldType();
425                if (elType.isSimpleType()) {
426                    p.setValue(ar);
427                } else if ("content".equals(elType.getName())) {
428                    // list of blobs
429                    List<Blob> blobs = new ArrayList<Blob>();
430                    if (ar.getClass().getComponentType() == String.class) { // transform
431                                                                            // strings
432                                                                            // to
433                                                                            // blobs
434                        for (Object obj : ar) {
435                            blobs.add(Blobs.createBlob(obj.toString()));
436                        }
437                    } else {
438                        for (Object obj : ar) {
439                            blobs.add((Blob) obj);
440                        }
441                    }
442                    p.setValue(blobs);
443                } else {
444                    // complex properties will be ignored
445                    // throw new
446                    // WebException("Cannot create complex lists properties from HTML forms");
447                }
448            }
449        } else if (p.isComplex()) {
450            if (p.getClass() == BlobProperty.class) {
451                // should be a file upload
452                Blob blob = null;
453                if (ar[0].getClass() == String.class) {
454                    blob = Blobs.createBlob(ar[0].toString());
455                } else {
456                    blob = (Blob) ar[0];
457                }
458                p.setValue(blob);
459            } else {
460                // complex properties will be ignored
461                // throw new WebException(
462                // "Cannot set complex properties from HTML forms. You need to set each sub-scalar property
463                // explicitely");
464            }
465        }
466    }
467
468    public VersioningOption getVersioningOption() {
469        String val = getString(VERSIONING);
470        if (val != null) {
471            return val.equals(MAJOR) ? VersioningOption.MAJOR : val.equals(MINOR) ? VersioningOption.MINOR : null;
472        }
473        return null;
474    }
475
476    public String getDocumentType() {
477        return getString(DOCTYPE);
478    }
479
480    public String getDocumentTitle() {
481        return getString(TITLE);
482    }
483
484    public <T extends Form> T validate(Class<T> type) throws ValidationException {
485        T proxy = FormManager.newProxy(type);
486        try {
487            proxy.load(this, proxy);
488            return proxy;
489        } catch (ValidationException e) {
490            e.setForm(proxy);
491            throw e;
492        }
493    }
494
495}