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.NuxeoException;
047import org.nuxeo.ecm.core.api.PropertyException;
048import org.nuxeo.ecm.core.api.VersioningOption;
049import org.nuxeo.ecm.core.api.model.Property;
050import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
051import org.nuxeo.ecm.core.schema.types.ListType;
052import org.nuxeo.ecm.core.schema.types.Type;
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 new NuxeoException("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 new NuxeoException("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            e.addInfo("Failed to fill document properties from request properties");
357            throw e;
358        }
359    }
360
361    @SuppressWarnings("unchecked")
362    public void fillDocumentFromForm(DocumentModel doc) throws PropertyException {
363        Map<String, String[]> map = request.getParameterMap();
364        for (Map.Entry<String, String[]> entry : map.entrySet()) {
365            String key = entry.getKey();
366            if (key.indexOf(':') > -1) { // an XPATH property
367                Property p;
368                try {
369                    p = doc.getProperty(key);
370                } catch (PropertyException e) {
371                    continue; // not a valid property
372                }
373                String[] ar = entry.getValue();
374                fillDocumentProperty(p, key, ar);
375            }
376        }
377    }
378
379    public void fillDocumentFromMultiPartForm(DocumentModel doc) throws PropertyException {
380        Map<String, List<FileItem>> map = getMultiPartItems();
381        for (Map.Entry<String, List<FileItem>> entry : map.entrySet()) {
382            String key = entry.getKey();
383            if (key.indexOf(':') > -1) { // an XPATH property
384                Property p;
385                try {
386                    p = doc.getProperty(key);
387                } catch (PropertyException e) {
388                    continue; // not a valid property
389                }
390                List<FileItem> list = entry.getValue();
391                if (list.isEmpty()) {
392                    fillDocumentProperty(p, key, null);
393                } else {
394                    Object[] ar = getMultiPartFormItems(list);
395                    fillDocumentProperty(p, key, ar);
396                }
397            }
398        }
399    }
400
401    static void fillDocumentProperty(Property p, String key, Object[] ar) throws PropertyException {
402        if (ar == null || ar.length == 0) {
403            p.remove();
404        } else if (p.isScalar()) {
405            p.setValue(ar[0]);
406        } else if (p.isList()) {
407            if (!p.isContainer()) { // an array
408                p.setValue(ar);
409            } else {
410                Type elType = ((ListType) p.getType()).getFieldType();
411                if (elType.isSimpleType()) {
412                    p.setValue(ar);
413                } else if ("content".equals(elType.getName())) {
414                    // list of blobs
415                    List<Blob> blobs = new ArrayList<Blob>();
416                    if (ar.getClass().getComponentType() == String.class) { // transform
417                                                                            // strings
418                                                                            // to
419                                                                            // blobs
420                        for (Object obj : ar) {
421                            blobs.add(Blobs.createBlob(obj.toString()));
422                        }
423                    } else {
424                        for (Object obj : ar) {
425                            blobs.add((Blob) obj);
426                        }
427                    }
428                    p.setValue(blobs);
429                } else {
430                    // complex properties will be ignored
431                    // throw new
432                    // WebException("Cannot create complex lists properties from HTML forms");
433                }
434            }
435        } else if (p.isComplex()) {
436            if (p.getClass() == BlobProperty.class) {
437                // should be a file upload
438                Blob blob = null;
439                if (ar[0].getClass() == String.class) {
440                    blob = Blobs.createBlob(ar[0].toString());
441                } else {
442                    blob = (Blob) ar[0];
443                }
444                p.setValue(blob);
445            } else {
446                // complex properties will be ignored
447                // throw new WebException(
448                // "Cannot set complex properties from HTML forms. You need to set each sub-scalar property
449                // explicitely");
450            }
451        }
452    }
453
454    public VersioningOption getVersioningOption() {
455        String val = getString(VERSIONING);
456        if (val != null) {
457            return val.equals(MAJOR) ? VersioningOption.MAJOR : val.equals(MINOR) ? VersioningOption.MINOR : null;
458        }
459        return null;
460    }
461
462    public String getDocumentType() {
463        return getString(DOCTYPE);
464    }
465
466    public String getDocumentTitle() {
467        return getString(TITLE);
468    }
469
470    public <T extends Form> T validate(Class<T> type) throws ValidationException {
471        T proxy = FormManager.newProxy(type);
472        try {
473            proxy.load(this, proxy);
474            return proxy;
475        } catch (ValidationException e) {
476            e.setForm(proxy);
477            throw e;
478        }
479    }
480
481}