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