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.stack = new Stack<String>();
084        this.contentType = contentType;
085        this.characterEncoding
086            = characterEncoding != null ? characterEncoding : "UTF-8";
087        if ("application/xhtml".equals(contentType)
088            || "application/xml".equals(contentType)
089            || "text/xml".equals(contentType)) {
090          xml = true;
091        }
092        helper = new HtmlWriterUtil(writer, characterEncoding);
093      }
094    
095      private String findValue(final Object value, final String property) {
096        if (value != null) {
097          return value instanceof String ? (String) value : value.toString();
098        } else if (property != null) {
099          if (component != null) {
100            final Object object = component.getAttributes().get(property);
101            if (object != null) {
102              return object instanceof String ? (String) object : object.toString();
103            } else {
104              return null;
105            }
106          } else {
107            final String trace = getCallingClassStackTraceElementString();
108            LOG.error("Don't know what to do! "
109                + "Property defined, but no component to get a value. "
110                + trace.substring(trace.indexOf('(')));
111            LOG.error("value = 'null'");
112            LOG.error("property = '" + property + "'");
113            return null;
114          }
115        } else {
116          final String trace = getCallingClassStackTraceElementString();
117          LOG.error("Don't know what to do! "
118              + "No value and no property defined. "
119              + trace.substring(trace.indexOf('(')));
120          LOG.error("value = 'null'");
121          LOG.error("property = 'null'");
122          return null;
123        }
124      }
125    
126      public void write(final char[] cbuf, final int off, final int len)
127          throws IOException {
128        writer.write(cbuf, off, len);
129      }
130    
131      @Override
132      public void write(String string) throws IOException {
133        closeOpenTag();
134        writer.write(string);
135      }
136    
137      @Override
138      public void write(int i) throws IOException {
139        closeOpenTag();
140        writer.write(i);
141      }
142    
143      @Override
144      public void write(char[] chars) throws IOException {
145        closeOpenTag();
146        writer.write(chars);
147      }
148    
149      @Override
150      public void write(String string, int i, int i1) throws IOException {
151        closeOpenTag();
152        writer.write(string, i, i1);
153      }
154    
155      public void close() throws IOException {
156        closeOpenTag();
157        writer.close();
158      }
159    
160      public void flush() throws IOException {
161        /*
162        From the api:
163        Flush any ouput buffered by the output method to the underlying Writer or OutputStream.
164        This method will not flush the underlying Writer or OutputStream;
165        it simply clears any values buffered by this ResponseWriter.
166         */
167        closeOpenTag();
168      }
169    
170      public void writeText(final Object text, final String property)
171          throws IOException {
172        closeOpenTag();
173        final String value = findValue(text, property);
174        if (xml) {
175          write(XmlUtils.escape(value));
176        } else {
177          helper.writeText(value);
178        }
179      }
180    
181      private void closeOpenTag() throws IOException {
182        if (startStillOpen) {
183          writer.write("\n>");
184          startStillOpen = false;
185        }
186      }
187    
188      public void writeText(final char[] text, final int offset, final int length)
189          throws IOException {
190        closeOpenTag();
191        if (xml) {
192          writer.write(XmlUtils.escape(String.valueOf(text)).toCharArray(), offset, length);
193    // FIXME: not nice:     XmlUtils.escape(text.toString()).toCharArray()
194        } else {
195          helper.writeText(text, offset, length);
196        }
197      }
198    
199      public void startDocument() throws IOException {
200        // nothing to do
201      }
202    
203      public void endDocument() throws IOException {
204        // nothing to do
205      }
206    
207      public String getContentType() {
208        return contentType;
209      }
210    
211      public String getCharacterEncoding() {
212        return characterEncoding;
213      }
214    
215      public void startElement(final String name, final UIComponent currentComponent)
216          throws IOException {
217        this.component = currentComponent;
218        stack.push(name);
219    //    closeOpenTag();
220        if (startStillOpen) {
221          writer.write("\n>");
222        }
223        writer.write("<");
224        writer.write(name);
225        startStillOpen = true;
226      }
227    
228      public void endElement(final String name) throws IOException {
229        if (LOG.isDebugEnabled()) {
230          LOG.debug("end Element: " + name);
231        }
232    
233        String top = "";
234        try {
235          top = stack.pop();
236        } catch (EmptyStackException e) {
237          LOG.error("Failed to close element \"" + name + "\"!");
238          throw e;
239        }
240        if (!top.equals(name)) {
241          final String trace = getCallingClassStackTraceElementString();
242          LOG.error("Element end with name='" + name + "' doesn't "
243              + "match with top element on the stack='" + top + "' "
244              + trace.substring(trace.indexOf('(')));
245        }
246    
247        if (EMPTY_TAG.contains(name)) {
248          if (xml) {
249            writer.write("\n/>");
250          } else {
251            writer.write("\n>");
252          }
253        } else {
254          if (startStillOpen) {
255            writer.write("\n>");
256          }
257          writer.write("</");
258          writer.write(name);
259    //      writer.write("\n>"); // FIXME: this makes problems with Tidy
260          writer.write(">");
261        }
262        startStillOpen = false;
263      }
264    
265      public void writeComment(final Object obj) throws IOException {
266        closeOpenTag();
267        String comment = obj.toString();
268        write("<!--");
269        if (comment.indexOf("--") < 0) {
270          write(comment);
271        } else {
272          String trace = getCallingClassStackTraceElementString();
273          LOG.warn(
274              "Comment must not contain the sequence '--', comment = '"
275                  + comment + "' " + trace.substring(trace.indexOf('(')));
276          write(StringUtils.replace(comment, "--", "++"));
277        }
278        write("-->");
279      }
280    
281      public ResponseWriter cloneWithWriter(final Writer originalWriter) {
282        return new TobagoResponseWriterImpl(
283            originalWriter, getContentType(), getCharacterEncoding());
284      }
285    
286      public void writeAttribute(final String name, final Object value, final String property)
287          throws IOException {
288    
289        final String attribute = findValue(value, property);
290        writeAttribute(name, attribute, true);
291      }
292    
293      private String getCallingClassStackTraceElementString() {
294        final StackTraceElement[] stackTrace = new Exception().getStackTrace();
295        int i = 1;
296        while (stackTrace[i].getClassName().equals(this.getClass().getName())) {
297          i++;
298        }
299        return stackTrace[i].toString();
300      }
301    
302      public void writeURIAttribute(final String s, final Object obj, final String s1)
303          throws IOException {
304        LOG.error("Not implemented yet!");
305      }
306    
307    // interface TobagoResponseWriter //////////////////////////////////////////////////////////////////////////////////
308    
309      public void writeAttribute(final String name, final String value, final boolean escape)
310          throws IOException {
311        if (!startStillOpen) {
312          String trace = getCallingClassStackTraceElementString();
313          String error = "Cannot write attribute when start-tag not open. "
314              + "name = '" + name + "' "
315              + "value = '" + value + "' "
316              + trace.substring(trace.indexOf('('));
317          LOG.error(error);
318          throw new IllegalStateException(error);
319        }
320    
321        if (value != null) {
322          writer.write(' ');
323          writer.write(name);
324          writer.write("=\"");
325          if (xml) {
326            writer.write(XmlUtils.escape(value));
327          } else {
328            if (escape) {
329              helper.writeAttributeValue(value);
330            } else {
331              writer.write(value);
332            }
333          }
334          writer.write('\"');
335        }
336      }
337    
338      public void writeClassAttribute() throws IOException {
339        Object clazz = component.getAttributes().get(ATTR_STYLE_CLASS);
340        if (clazz != null) {
341          writeAttribute(HtmlAttributes.CLASS, clazz.toString(), false);
342        }
343      }
344    
345      public void writeStyleAttribute() throws IOException {
346        Object style = component.getAttributes().get(ATTR_STYLE);
347        if (style != null) {
348          writeAttribute(HtmlAttributes.STYLE, style.toString(), false);
349        }
350      }
351    }