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