001/*
002 * (C) Copyright 2008 JBoss and others.
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 *     Original file from org.jboss.seam.excel.ui.UICell.java in jboss-seam-excel
016 *     Anahide Tchertchian
017 */
018package org.nuxeo.ecm.platform.ui.web.component.seam;
019
020import java.io.IOException;
021import java.io.StringWriter;
022import java.text.DateFormat;
023import java.text.ParseException;
024import java.util.Locale;
025
026import javax.el.ELException;
027import javax.el.ValueExpression;
028import javax.faces.FacesException;
029import javax.faces.component.UIComponent;
030import javax.faces.context.FacesContext;
031import javax.faces.context.ResponseWriter;
032
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.jboss.seam.core.Interpolator;
037import org.jboss.seam.excel.ExcelWorkbookException;
038import org.jboss.seam.ui.util.JSF;
039import org.nuxeo.ecm.platform.ui.web.util.NXHtmlResponseWriter;
040
041import com.sun.faces.config.WebConfiguration;
042
043/**
044 * Override of Seam cell component to control HTML encoding of accents in excel, and to improve data type guessing when
045 * using dates or numbers.
046 *
047 * @since 5.5
048 */
049public class UICellExcel extends org.jboss.seam.excel.ui.UICell {
050
051    private static final Log log = LogFactory.getLog(UICellExcel.class);
052
053    public static final String DEFAULT_CONTENT_TYPE = "text/html";
054
055    public static final String DEFAULT_CHARACTER_ENCODING = "utf-8";
056
057    // add field again as it's private in parent class
058    protected Object value;
059
060    /**
061     * Force type attribute, added here to ensure value expression resolution
062     */
063    protected String forceType;
064
065    /**
066     * Style attribute, added here to ensure value expression resolution
067     */
068    protected String style;
069
070    @Override
071    public Object getValue() {
072        Object theValue = valueOf("value", value);
073        if (theValue == null) {
074            try {
075                theValue = cmp2String(FacesContext.getCurrentInstance(), this);
076                String forceType = getForceType();
077                if (forceType != null && !forceType.isEmpty()) {
078                    theValue = convertStringToTargetType((String) theValue, forceType);
079                }
080            } catch (IOException e) {
081                String message = Interpolator.instance().interpolate("Could not render cell #0", getId());
082                throw new ExcelWorkbookException(message, e);
083            }
084        } else {
085            theValue = theValue.toString();
086        }
087        return theValue;
088    }
089
090    @Override
091    public void setValue(Object value) {
092        this.value = value;
093    }
094
095    /**
096     * Converts string value as returned by widget to the target type for an accurate cell format in the XLS/CSV export.
097     * <ul>
098     * <li>If force type is set to "number", convert value to a double (null if empty).</li>
099     * <li>If force type is set to "bool", convert value to a boolean (null if empty).</li>
100     * <li>If force type is set to "date", convert value to a date using most frequent date parsers using the short,
101     * medium, long and full formats and current locale, trying first with time information and after with only date
102     * information. Returns null if date is empty or could not be parsed.</li>
103     * </ul>
104     *
105     * @since 5.6
106     */
107    protected Object convertStringToTargetType(String value, String forceType) {
108        if (CellType.number.name().equals(forceType)) {
109            if (StringUtils.isBlank(value)) {
110                return null;
111            }
112            return Double.valueOf(value);
113        } else if (CellType.date.name().equals(forceType)) {
114            if (StringUtils.isBlank(value)) {
115                return null;
116            }
117            Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
118            int[] formats = { DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL };
119            for (int format : formats) {
120                try {
121                    return DateFormat.getDateTimeInstance(format, format, locale).parse(value);
122                } catch (ParseException e) {
123                    // ignore
124                }
125                try {
126                    return DateFormat.getDateInstance(format, locale).parse(value);
127                } catch (ParseException e) {
128                    // ignore
129                }
130            }
131            log.warn("Could not convert value to a date instance: " + value);
132            return null;
133        } else if (CellType.bool.name().equals(forceType)) {
134            if (StringUtils.isBlank(value)) {
135                return null;
136            }
137            return Boolean.valueOf(value);
138        }
139        return value;
140    }
141
142    /**
143     * Helper method for rendering a component (usually on a facescontext with a caching reponsewriter)
144     *
145     * @param facesContext The faces context to render to
146     * @param component The component to render
147     * @return The textual representation of the component
148     * @throws IOException If the JSF helper class can't render the component
149     */
150    public static String cmp2String(FacesContext facesContext, UIComponent component) throws IOException {
151        ResponseWriter oldResponseWriter = facesContext.getResponseWriter();
152        String contentType = oldResponseWriter != null ? oldResponseWriter.getContentType() : DEFAULT_CONTENT_TYPE;
153        String characterEncoding = oldResponseWriter != null ? oldResponseWriter.getCharacterEncoding()
154                : DEFAULT_CHARACTER_ENCODING;
155        StringWriter cacheingWriter = new StringWriter();
156
157        // XXX: create a response writer by hand, to control html escaping of
158        // iso characters
159        // take default values for these confs
160        Boolean scriptHiding = Boolean.FALSE;
161        Boolean scriptInAttributes = Boolean.TRUE;
162        // force escaping to true
163        WebConfiguration.DisableUnicodeEscaping escaping = WebConfiguration.DisableUnicodeEscaping.True;
164        ResponseWriter newResponseWriter = new NXHtmlResponseWriter(cacheingWriter, contentType, characterEncoding,
165                scriptHiding, scriptInAttributes, escaping);
166        // ResponseWriter newResponseWriter = renderKit.createResponseWriter(
167        // cacheingWriter, contentType, characterEncoding);
168
169        facesContext.setResponseWriter(newResponseWriter);
170        JSF.renderChild(facesContext, component);
171        if (oldResponseWriter != null) {
172            facesContext.setResponseWriter(oldResponseWriter);
173        }
174        cacheingWriter.flush();
175        cacheingWriter.close();
176        return cacheingWriter.toString();
177    }
178
179    /**
180     * Returns the style attribute, used to format cells with a specific {@link #forceType}. Sample value for dates
181     * formatting: "xls-format-mask: #{nxu:basicDateFormatter()};".
182     *
183     * @since 5.6
184     */
185    @Override
186    public String getStyle() {
187        if (style != null) {
188            return style;
189        }
190        ValueExpression ve = getValueExpression("style");
191        if (ve != null) {
192            try {
193                return (String) ve.getValue(getFacesContext().getELContext());
194            } catch (ELException e) {
195                throw new FacesException(e);
196            }
197        } else {
198            return null;
199        }
200    }
201
202    /**
203     * @since 5.6
204     */
205    @Override
206    public void setStyle(String style) {
207        this.style = style;
208    }
209
210    /**
211     * Returns the force type attribute, used to force cell type to "date" or "number" for instance.
212     *
213     * @since 5.6
214     */
215    public String getForceType() {
216        if (forceType != null) {
217            return forceType;
218        }
219        ValueExpression ve = getValueExpression("forceType");
220        if (ve != null) {
221            try {
222                return (String) ve.getValue(getFacesContext().getELContext());
223            } catch (ELException e) {
224                throw new FacesException(e);
225            }
226        } else {
227            return null;
228        }
229    }
230
231    /**
232     * @since 5.6
233     */
234    public void setForceType(String forceType) {
235        this.forceType = forceType;
236    }
237
238    // state holder
239
240    @Override
241    public void restoreState(FacesContext context, Object state) {
242        Object[] values = (Object[]) state;
243        super.restoreState(context, values[0]);
244        forceType = (String) values[1];
245        style = (String) values[2];
246    }
247
248    @Override
249    public Object saveState(FacesContext context) {
250        return new Object[] { super.saveState(context), forceType, style };
251    }
252
253}