View Javadoc

1   /**
2    * Copyright 2007 Art Technology Group, Inc (ATG)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    *
7    * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and limitations under the License.
13   */
14  
15  package atg.taglib.json;
16  
17  import atg.taglib.json.util.JSONException;
18  
19  import java.util.Stack;
20  
21  import javax.servlet.ServletContext;
22  import javax.servlet.jsp.JspException;
23  import javax.servlet.jsp.JspWriter;
24  import javax.servlet.jsp.PageContext;
25  import javax.servlet.jsp.tagext.SimpleTagSupport;
26  
27  /**
28   * Abstract base class for all JSON tags
29   *
30   * @author James Wiltshire
31   * @version $Id$
32   */
33  
34  public abstract class JsonBaseTag extends SimpleTagSupport
35  {
36    private static final String JSON_OBJECT_STACK_KEY = "atg.taglib.json.objectStack";
37    private static final String PRETTY_PRINT_KEY = "atg.taglib.json.prettyPrint";
38    private static final String ESCAPE_XML_KEY="atg.taglib.json.escapeXml";
39    private static final String CURRENT_ESCAPE_XML_VALUE_KEY="atg.taglib.json.escapeXml.currentValue";
40    
41    private static final int JSON_OBJECT_STACK_SCOPE = PageContext.REQUEST_SCOPE;
42    protected static final int PRETTY_PRINT_INDENT=2;
43    
44    private static final boolean ESCAPE_XML_DEFAULT=true;
45    private static final boolean TRIM_DEFAULT = true;
46    private static final boolean PRETTY_PRINT_DEFAULT=false;
47    
48    protected String mName;
49  
50    /**
51     * Gets the Name
52     * @return the Name
53     */
54    public String getName()
55    {
56      return mName;
57    }
58  
59    /**
60     * Sets the Name
61     *
62     * @param pName The Name to set
63     */
64    public void setName(String pName)
65    {
66      mName = pName;
67    }
68    
69    protected boolean mTrim = TRIM_DEFAULT;
70  
71    /**
72     * Gets the Trim
73     * @return the Trim
74     */
75    public boolean getTrim()
76    {
77      return mTrim;
78    }
79  
80    /**
81     * Sets the Trim
82     *
83     * @param pTrim The Trim to set
84     */
85    public void setTrim(boolean pTrim)
86    {
87      mTrim = pTrim;
88    } 
89    
90    protected boolean mNewStackCreated=false;
91    
92    /**
93     * Sets the PrettyPrint flag. If set, then any JSON output should be nicely formatted
94     *
95     * @param pPrettyPrint The PrettyPrint to set
96     */
97    public void setPrettyPrint(boolean pPrettyPrint)
98    {
99      getJspContext().setAttribute(PRETTY_PRINT_KEY, Boolean.valueOf(pPrettyPrint));
100   }
101   
102   /**
103    * Get the number of spaces that should be used to indent the pretty-printed output,
104    * or 0 if no pretty-printing should be used
105    *
106    * @return The number of spaces to indent when pretty printing
107    */
108   public int getPrettyPrintIndentFactor()
109   {
110     boolean prettyPrint=getBooleanDefaultValue(PRETTY_PRINT_KEY, Boolean.valueOf(PRETTY_PRINT_DEFAULT));
111     return (prettyPrint?PRETTY_PRINT_INDENT:0);
112   }
113 
114   protected Boolean mEscapeXmlOriginalValue=null;
115   protected boolean mEscapeXmlValueSet=false;
116   
117   /**
118    * Gets the value of the EscapeXml flag. This can be set on any tag by setting the
119    * <code>escapeXml</code> attribute to <code>true</code>, or it can be set by setting
120    * a page attribute <code>atg.taglib.json.escapeXml</code>. When set on a tag it will
121    * recursively apply to all sub-tags, but may be overriden by any sub-tag.
122    * <br/>
123    * Defaults to <code>true</code>
124    * 
125    * @return the EscapeXml flag value
126    */
127   public boolean getEscapeXml()
128   {
129     Boolean escapeXml=(Boolean)getJspContext().getAttribute
130       (CURRENT_ESCAPE_XML_VALUE_KEY,PageContext.REQUEST_SCOPE);
131     if (escapeXml==null){
132       escapeXml = getEscapeXmlDefault();
133     }
134     return escapeXml.booleanValue();
135   }
136 
137   /**
138    * Sets the EscapeXml flag. If set, then any output will be xml-escaped
139    *
140    * @param pEscapeXml The EscapeXml to set
141    */
142   public void setEscapeXml(boolean pEscapeXml)
143   {
144     mEscapeXmlValueSet=true;
145     mEscapeXmlOriginalValue=(Boolean)getJspContext().getAttribute
146       (CURRENT_ESCAPE_XML_VALUE_KEY,PageContext.REQUEST_SCOPE);
147     getJspContext().setAttribute
148       (CURRENT_ESCAPE_XML_VALUE_KEY, Boolean.valueOf(pEscapeXml),PageContext.REQUEST_SCOPE);
149   }
150   
151   /**
152    * Get the default value of the escapeXml property
153    *
154    * @return the default escapeXml value
155    */
156   public Boolean getEscapeXmlDefault()
157   {
158     return Boolean.valueOf(getBooleanDefaultValue(ESCAPE_XML_KEY, Boolean.valueOf(ESCAPE_XML_DEFAULT)));
159   }
160   
161   /**
162    * Replace the escapeXml value with the original value if this tag modified it
163    *
164    */
165   protected void resetEscapeXmlValue()
166   {
167     if (mEscapeXmlValueSet){
168       getJspContext().setAttribute
169         (CURRENT_ESCAPE_XML_VALUE_KEY, mEscapeXmlOriginalValue, PageContext.REQUEST_SCOPE);
170     }
171   }
172   
173   
174   /**
175    * Get the JSON object stack. This is stored as an attribute in the
176    * page context. Request scope is used so that jsp:include can be used
177    * to nest json: tags in included pages.
178    * <p>
179    * If an entity stack does not exists, then this method will create a new one
180    * 
181    * 
182    * @return The JSON Entity Stack
183    * @exception JspException If there is a problem creating a new entity stack
184    */
185   protected Stack getEntityStack() throws JspException{
186     Stack stack=(Stack)getJspContext().getAttribute(JSON_OBJECT_STACK_KEY, JSON_OBJECT_STACK_SCOPE);
187     if (stack==null){
188       stack = createEntityStack();
189       mNewStackCreated=true;
190     }
191     return stack;
192   }
193   
194   /**
195    * Create a new entity stack and set it in pageContext
196    *
197    * @return The newly created JSON entity statck
198    */
199   protected Stack createEntityStack() throws JspException{      
200     Stack stack = new Stack();
201     getJspContext().setAttribute(JSON_OBJECT_STACK_KEY, stack, JSON_OBJECT_STACK_SCOPE);
202     return stack;
203   }
204   
205   /**
206    * Remove the entity stack object from page context
207    *
208    */
209   protected void removeEntityStack(){
210     getJspContext().removeAttribute(JSON_OBJECT_STACK_KEY, JSON_OBJECT_STACK_SCOPE);
211   }
212   
213   /**
214    * Get the topmost JSONObject or JSONArray from the stack
215    *
216    * @return the topmost <code>JsonEntity</code> on the stack
217    * @throws JspException if unable to get the object stack
218    */
219   protected JsonEntity getCurrentEntity() throws JspException{
220     return (JsonEntity)getEntityStack().peek();
221   }
222   
223   /**
224    * Does an entity stack exist?
225    *
226    * @return <code>true</code> if an entity stack is found in pageContext, <code>false</code> otherwise
227    */
228   protected boolean entityStackExists()
229   {
230     return (getJspContext().getAttribute(JSON_OBJECT_STACK_KEY, JSON_OBJECT_STACK_SCOPE)!=null);
231   }
232   
233   /**
234    * Is this tag the root <code>json:</code> tag?
235    *
236    * @return <code>true</code> if this tag instance is the root tag, <code>false</code> otherwise
237    */
238   protected boolean isRootTag()
239   {
240     // If a new stack was created by this tag instance, then it's the top level tag
241     return mNewStackCreated;
242   }
243   
244   /**
245    * Get the default value for a parameter. This will search each of the following locations in order,
246    * returning the first value that it finds.
247    * <ol>
248    *   <li>page/request/session/application attributes. Uses findAttribute to try and find the attribute</li>
249    *   <li>Servlet Context initParams - as set in a web.xml context-param</li>
250    *   <li>The Default Constant value passed in to this method</li>
251    * </ol>
252    *
253    * @param pParamKey The key of the param.
254    * @return
255    */
256   protected Object getDefaultValue(String pParamKey, Object pDefaultConstant)
257   {
258     Object value;
259     
260     // Attribute
261     value=getJspContext().findAttribute(pParamKey);
262     if (value!=null){
263       return value;
264     }
265     
266     // Servlet Context init param
267     ServletContext servletContext=((PageContext)getJspContext()).getServletContext();
268     value=servletContext.getInitParameter(pParamKey);
269     if (value!=null){
270       return value;
271     }
272     
273     // Default Constant
274     return pDefaultConstant;
275   }
276   
277   /**
278    * Get the default value of a parameters as a boolean
279    *
280    * @param pParamKey
281    * @param pDefaultConstant
282    * @return
283    */
284   private boolean getBooleanDefaultValue(String pParamKey, Object pDefaultConstant)
285   {
286     Object defaultValue=getDefaultValue(pParamKey, pDefaultConstant);
287     if (defaultValue instanceof String){
288       return Boolean.valueOf((String)defaultValue).booleanValue();
289     }
290     else if (defaultValue instanceof Boolean){
291       return ((Boolean)defaultValue).booleanValue();
292     }
293     else{
294       return false;
295     }
296   }
297   
298   /**
299    * Trim and EscapeXml chars in the value depending on whether the <code>trim</code> and
300    * <code>escapeXml</code> attributes have been set
301    *
302    * @param pValue The value to trim and xml-escape
303    * @return The processed value
304    */
305   protected Object trimAndEscapeValue(Object pValue)
306   {
307     // If the value isn't a string, just return as we can't process it
308     if (pValue instanceof String == false){
309       return pValue;
310     }
311     
312     String result=(String)pValue;
313     
314     
315     // Trim if trim attribute is set
316     if (getTrim()){
317       result=result.trim();
318     } 
319     
320     // Escape all XML characters (as defined in JSTL1.1 spec, page 23)
321     if (getEscapeXml()){
322       result=result
323         .replaceAll("&", "&amp;")
324         .replaceAll("<", "&lt;")
325         .replaceAll(">", "&gt;")        
326         .replaceAll("'", "&#039;")
327         .replaceAll("\"", "&#034;");
328     }
329     
330     return result;
331   }
332   
333   /**
334    * Process the end of a tag. Check to see if this is the top level tag that created the
335    * entity stack, and if so, render the JSON data to the output stream.
336    * 
337    * @param pNewEntity The entity that has just been created by this tag 
338    * @throws JspException 
339    * @throws JSONException 
340    *
341    */
342   protected void processTagEnd(JsonEntity pNewEntity) throws JspException, JSONException
343   {        
344     if (isRootTag()){
345       try{
346         JspWriter out = getJspContext().getOut();
347         
348         // Render JSON data to output stream, pretty printing if prettyPrintIndentFactor is > 0
349         String jsonText=(getPrettyPrintIndentFactor()>0)?
350             pNewEntity.toString(getPrettyPrintIndentFactor()) : pNewEntity.toString();
351         out.write(jsonText);
352       }
353       catch (Exception e){
354         throw new JspException(Messages.getString("atg.taglib.json.error.base.0"),e);
355       }
356       finally{
357         // Cleanup
358         removeEntityStack();
359       }      
360     }
361     else{
362       // This tag instance isn't the top level tag, so add the newly created entity to the parent
363       // object on the top of the entity stack
364       getCurrentEntity().add(pNewEntity.getWrappedObject(), getName());
365     }
366     
367     resetEscapeXmlValue();    
368   }
369 }