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}