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