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