001    package org.apache.myfaces.tobago.webapp;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one or more
005     * contributor license agreements.  See the NOTICE file distributed with
006     * this work for additional information regarding copyright ownership.
007     * The ASF licenses this file to You under the Apache License, Version 2.0
008     * (the "License"); you may not use this file except in compliance with
009     * the License.  You may obtain a copy of the License at
010     *
011     *      http://www.apache.org/licenses/LICENSE-2.0
012     *
013     * Unless required by applicable law or agreed to in writing, software
014     * distributed under the License is distributed on an "AS IS" BASIS,
015     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016     * See the License for the specific language governing permissions and
017     * limitations under the License.
018     */
019    
020    import org.apache.commons.lang.StringUtils;
021    import org.apache.commons.logging.Log;
022    import org.apache.commons.logging.LogFactory;
023    import static org.apache.myfaces.tobago.TobagoConstants.ATTR_STYLE;
024    import static org.apache.myfaces.tobago.TobagoConstants.ATTR_STYLE_CLASS;
025    import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
026    import org.apache.myfaces.tobago.renderkit.html.HtmlConstants;
027    import org.apache.myfaces.tobago.util.HtmlWriterUtil;
028    import org.apache.myfaces.tobago.util.XmlUtils;
029    
030    import javax.faces.component.UIComponent;
031    import javax.faces.context.ResponseWriter;
032    import java.io.IOException;
033    import java.io.Writer;
034    import java.util.Arrays;
035    import java.util.HashSet;
036    import java.util.Set;
037    import java.util.Stack;
038    import java.util.EmptyStackException;
039    
040    public class TobagoResponseWriterImpl extends TobagoResponseWriter {
041    
042      private static final Log LOG = LogFactory.getLog(TobagoResponseWriterImpl.class);
043    
044      private static final Set<String> EMPTY_TAG = new HashSet<String>(Arrays.asList(
045          HtmlConstants.BR,
046          HtmlConstants.AREA,
047          HtmlConstants.LINK,
048          HtmlConstants.IMG,
049          HtmlConstants.PARAM,
050          HtmlConstants.HR,
051          HtmlConstants.INPUT,
052          HtmlConstants.COL,
053          HtmlConstants.BASE,
054          HtmlConstants.META));
055    
056      private Writer writer;
057    
058      private UIComponent component;
059    
060      private boolean startStillOpen;
061    
062      private String contentType;
063    
064      private String characterEncoding;
065    
066      private Stack<String> stack;
067    
068      /**
069       * use XML instead HMTL
070       */
071      private boolean xml;
072    
073      private HtmlWriterUtil helper;
074    
075      public TobagoResponseWriterImpl(final Writer writer, final String contentType,
076          final String characterEncoding) {
077    //    LOG.info("new TobagoResponseWriterImpl!");
078    //    final StackTraceElement[] stackTrace = new Exception().getStackTrace();
079    //    for (int i = 1; i < stackTrace.length && i < 5; i++) {
080    //      LOG.info("  " + stackTrace[i].toString());
081    //    }
082        this.writer = writer;
083        this.component = null;
084        this.stack = new Stack<String>();
085        this.contentType = contentType;
086        this.characterEncoding
087            = characterEncoding != null ? characterEncoding : "UTF-8";
088        if ("application/xhtml".equals(contentType)
089            || "application/xml".equals(contentType)
090            || "text/xml".equals(contentType)) {
091          xml = true;
092        }
093        helper = new HtmlWriterUtil(writer, characterEncoding);
094      }
095    
096      private String findValue(final Object value, final String property) {
097        if (value != null) {
098          return value instanceof String ? (String) value : value.toString();
099        } else if (property != null) {
100          if (component != null) {
101            final Object object = component.getAttributes().get(property);
102            if (object != null) {
103              return object instanceof String ? (String) object : object.toString();
104            } else {
105              return null;
106            }
107          } else {
108            final String trace = getCallingClassStackTraceElementString();
109            LOG.error("Don't know what to do! "
110                + "Property defined, but no component to get a value. "
111                + trace.substring(trace.indexOf('(')));
112            LOG.error("value = 'null'");
113            LOG.error("property = '" + property + "'");
114            return null;
115          }
116        } else {
117          final String trace = getCallingClassStackTraceElementString();
118          LOG.error("Don't know what to do! "
119              + "No value and no property defined. "
120              + trace.substring(trace.indexOf('(')));
121          LOG.error("value = 'null'");
122          LOG.error("property = 'null'");
123          return null;
124        }
125      }
126    
127      public void write(final char[] cbuf, final int off, final int len)
128          throws IOException {
129        writer.write(cbuf, off, len);
130      }
131    
132      @Override
133      public void write(String string) throws IOException {
134        closeOpenTag();
135        writer.write(string);
136      }
137    
138      @Override
139      public void write(int i) throws IOException {
140        closeOpenTag();
141        writer.write(i);
142      }
143    
144      @Override
145      public void write(char[] chars) throws IOException {
146        closeOpenTag();
147        writer.write(chars);
148      }
149    
150      @Override
151      public void write(String string, int i, int i1) throws IOException {
152        closeOpenTag();
153        writer.write(string, i, i1);
154      }
155    
156      public void close() throws IOException {
157        closeOpenTag();
158        writer.close();
159      }
160    
161      public void flush() throws IOException {
162        /*
163        From the api:
164        Flush any ouput buffered by the output method to the underlying Writer or OutputStream.
165        This method will not flush the underlying Writer or OutputStream;
166        it simply clears any values buffered by this ResponseWriter.
167         */
168        closeOpenTag();
169      }
170    
171      public void writeText(final Object text, final String property)
172          throws IOException {
173        closeOpenTag();
174        final String value = findValue(text, property);
175        if (xml) {
176          write(XmlUtils.escape(value));
177        } else {
178          helper.writeText(value);
179        }
180      }
181    
182      private void closeOpenTag() throws IOException {
183        if (startStillOpen) {
184          writer.write("\n>");
185          startStillOpen = false;
186        }
187      }
188    
189      public void writeText(final char[] text, final int offset, final int length)
190          throws IOException {
191        closeOpenTag();
192        if (xml) {
193          writer.write(XmlUtils.escape(String.valueOf(text)).toCharArray(), offset, length);
194    // FIXME: not nice:     XmlUtils.escape(text.toString()).toCharArray()
195        } else {
196          helper.writeText(text, offset, length);
197        }
198      }
199    
200      public void startDocument() throws IOException {
201        // nothing to do
202      }
203    
204      public void endDocument() throws IOException {
205        // nothing to do
206      }
207    
208      public String getContentType() {
209        return contentType;
210      }
211    
212      public String getCharacterEncoding() {
213        return characterEncoding;
214      }
215    
216      public void startElement(final String name, final UIComponent currentComponent)
217          throws IOException {
218        this.component = currentComponent;
219        stack.push(name);
220    //    closeOpenTag();
221        if (startStillOpen) {
222          writer.write("\n>");
223        }
224        writer.write("<");
225        writer.write(name);
226        startStillOpen = true;
227      }
228    
229      public void endElement(final String name) throws IOException {
230        if (LOG.isDebugEnabled()) {
231          LOG.debug("end Element: " + name);
232        }
233    
234        String top = "";
235        try {
236          top = stack.pop();
237        } catch (EmptyStackException e) {
238          LOG.error("Failed to close element \"" + name + "\"!");
239          throw e;
240        }
241        if (!top.equals(name)) {
242          final String trace = getCallingClassStackTraceElementString();
243          LOG.error("Element end with name='" + name + "' doesn't "
244              + "match with top element on the stack='" + top + "' "
245              + trace.substring(trace.indexOf('(')));
246        }
247    
248        if (EMPTY_TAG.contains(name)) {
249          if (xml) {
250            writer.write("\n/>");
251          } else {
252            writer.write("\n>");
253          }
254        } else {
255          if (startStillOpen) {
256            writer.write("\n>");
257          }
258          writer.write("</");
259          writer.write(name);
260    //      writer.write("\n>"); // FIXME: this makes problems with Tidy
261          writer.write(">");
262        }
263        startStillOpen = false;
264      }
265    
266      public void writeComment(final Object obj) throws IOException {
267        closeOpenTag();
268        String comment = obj.toString();
269        write("<!--");
270        if (comment.indexOf("--") < 0) {
271          write(comment);
272        } else {
273          String trace = getCallingClassStackTraceElementString();
274          LOG.warn(
275              "Comment must not contain the sequence '--', comment = '"
276                  + comment + "' " + trace.substring(trace.indexOf('(')));
277          write(StringUtils.replace(comment, "--", "++"));
278        }
279        write("-->");
280      }
281    
282      public ResponseWriter cloneWithWriter(final Writer originalWriter) {
283        return new TobagoResponseWriterImpl(
284            originalWriter, getContentType(), getCharacterEncoding());
285      }
286    
287      public void writeAttribute(final String name, final Object value, final String property)
288          throws IOException {
289    
290        final String attribute = findValue(value, property);
291        writeAttribute(name, attribute, true);
292      }
293    
294      private String getCallingClassStackTraceElementString() {
295        final StackTraceElement[] stackTrace = new Exception().getStackTrace();
296        int i = 1;
297        while (stackTrace[i].getClassName().equals(this.getClass().getName())) {
298          i++;
299        }
300        return stackTrace[i].toString();
301      }
302    
303      public void writeURIAttribute(final String s, final Object obj, final String s1)
304          throws IOException {
305        LOG.error("Not implemented yet!");
306      }
307    
308    // interface TobagoResponseWriter //////////////////////////////////////////////////////////////////////////////////
309    
310      public void writeAttribute(final String name, final String value, final boolean escape)
311          throws IOException {
312        if (!startStillOpen) {
313          String trace = getCallingClassStackTraceElementString();
314          String error = "Cannot write attribute when start-tag not open. "
315              + "name = '" + name + "' "
316              + "value = '" + value + "' "
317              + trace.substring(trace.indexOf('('));
318          LOG.error(error);
319          throw new IllegalStateException(error);
320        }
321    
322        if (value != null) {
323          writer.write(' ');
324          writer.write(name);
325          writer.write("=\"");
326          if (xml) {
327            writer.write(XmlUtils.escape(value));
328          } else {
329            if (escape) {
330              helper.writeAttributeValue(value);
331            } else {
332              writer.write(value);
333            }
334          }
335          writer.write('\"');
336        }
337      }
338    
339      public void writeClassAttribute() throws IOException {
340        Object clazz = component.getAttributes().get(ATTR_STYLE_CLASS);
341        if (clazz != null) {
342          writeAttribute(HtmlAttributes.CLASS, clazz.toString(), false);
343        }
344      }
345    
346      public void writeStyleAttribute() throws IOException {
347        Object style = component.getAttributes().get(ATTR_STYLE);
348        if (style != null) {
349          writeAttribute(HtmlAttributes.STYLE, style.toString(), false);
350        }
351      }
352    }