001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.jsonpart;
029
030import org.opencms.gwt.shared.CmsGwtConstants;
031import org.opencms.json.JSONArray;
032import org.opencms.json.JSONObject;
033import org.opencms.main.CmsLog;
034
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.OutputStreamWriter;
038import java.io.PrintWriter;
039import java.util.Enumeration;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.Vector;
044
045import javax.servlet.Filter;
046import javax.servlet.FilterChain;
047import javax.servlet.FilterConfig;
048import javax.servlet.ServletException;
049import javax.servlet.ServletOutputStream;
050import javax.servlet.ServletRequest;
051import javax.servlet.ServletResponse;
052import javax.servlet.WriteListener;
053import javax.servlet.http.HttpServletRequest;
054import javax.servlet.http.HttpServletRequestWrapper;
055import javax.servlet.http.HttpServletResponse;
056import javax.servlet.http.HttpServletResponseWrapper;
057
058import org.apache.commons.logging.Log;
059
060import com.google.common.collect.Maps;
061import com.google.common.collect.Sets;
062
063/**
064 * This servlet filter post-processes the response output for requests with the parameter '__json=true'.<p>
065 *
066 * It converts the encoded JSON parts generated by the &lt;cms:jsonpart&gt; tag, converts them to JSON, writes them to the response,
067 * and throws everything else away.
068 */
069public class CmsJsonPartFilter implements Filter {
070
071    /**
072     * Request wrapper used to disable direct edit functionality.<p>
073     */
074    class RequestWrapper extends HttpServletRequestWrapper {
075
076        /**
077         * Creates a new instance.<p>
078         *
079         * @param request the wrapped request
080         */
081        public RequestWrapper(HttpServletRequest request) {
082            super(request);
083        }
084
085        /**
086         * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
087         */
088        @Override
089        public String getParameter(String name) {
090
091            if (CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT.equals(name)) {
092                return Boolean.TRUE.toString();
093            } else {
094                return super.getParameter(name);
095            }
096        }
097
098        /**
099         * @see javax.servlet.ServletRequestWrapper#getParameterMap()
100         */
101        @Override
102        public Map<String, String[]> getParameterMap() {
103
104            Map<String, String[]> result = Maps.newHashMap(super.getParameterMap());
105            result.put(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT, new String[] {"true"});
106            return result;
107        }
108
109        /**
110         * @see javax.servlet.ServletRequestWrapper#getParameterNames()
111         */
112        @Override
113        public Enumeration<String> getParameterNames() {
114
115            Set<String> keys = Sets.newHashSet();
116            keys.add(CmsGwtConstants.PARAM_DISABLE_DIRECT_EDIT);
117            return new Vector<String>(keys).elements();
118        }
119
120        /**
121         * @see javax.servlet.ServletRequestWrapper#getParameterValues(java.lang.String)
122         */
123        @Override
124        public String[] getParameterValues(String name) {
125
126            return super.getParameterValues(name);
127        }
128    }
129
130    /**
131     * A response wrapper used to capture output so we can post-process it.<p>
132     */
133    class ResponseWrapper extends HttpServletResponseWrapper {
134
135        /** The stream used to collect the output bytes. */
136        ByteArrayOutputStream m_byteStream = new java.io.ByteArrayOutputStream();
137
138        /** A writer used to collect string-based output. */
139        PrintWriter m_printWriter;
140
141        /**
142         * Creates a new wrapper instance for the given response.<p>
143         *
144         * @param response the original response
145         */
146        public ResponseWrapper(HttpServletResponse response) {
147            super(response);
148        }
149
150        /**
151         * Gets the bytes written so far.<p>
152         *
153         * @return the bytes written
154         */
155        public byte[] getBytes() {
156
157            if (m_printWriter != null) {
158                m_printWriter.flush();
159            }
160            return m_byteStream.toByteArray();
161        }
162
163        /**
164         * @see javax.servlet.ServletResponseWrapper#getOutputStream()
165         */
166        @Override
167        public ServletOutputStream getOutputStream() {
168
169            return new ServletOutputStream() {
170
171                /**
172                 * @see java.io.OutputStream#write(byte[])
173                 */
174                @Override
175                public void write(byte[] b) throws IOException {
176
177                    m_byteStream.write(b);
178                }
179
180                /**
181                 * @see java.io.OutputStream#write(byte[], int, int)
182                 */
183                @Override
184                public void write(byte[] b, int off, int len) {
185
186                    m_byteStream.write(b, off, len);
187                }
188
189                /**
190                 * @see java.io.OutputStream#write(int)
191                 */
192                @Override
193                public void write(int b) {
194
195                    m_byteStream.write(b);
196                }
197
198                /**
199                 * @see javax.servlet.ServletOutputStream#isReady()
200                 */
201                @Override
202                public boolean isReady() {
203
204                    return null != m_byteStream;
205                }
206
207                /**
208                 * @see javax.servlet.ServletOutputStream#setWriteListener(javax.servlet.WriteListener)
209                 */
210                @Override
211                public void setWriteListener(WriteListener writeListener) {
212                }
213            };
214        }
215
216        /**
217         * @see javax.servlet.ServletResponseWrapper#getWriter()
218         */
219        @Override
220        public PrintWriter getWriter() throws IOException {
221
222            if (m_printWriter == null) {
223                m_printWriter = new PrintWriter(
224                    new OutputStreamWriter(m_byteStream, getResponse().getCharacterEncoding()));
225            }
226            return m_printWriter;
227        }
228
229        /**
230         * This method does nothing, we want to ignore calls to setContentLength because we want to postprocess
231         * the output, resulting in a different length.
232         *
233         * @see javax.servlet.ServletResponseWrapper#setContentLength(int)
234         */
235        @Override
236        public void setContentLength(int len) {
237            // ignore
238        }
239    }
240
241    /** The static log object for this class. */
242    static final Log LOG = CmsLog.getLog(CmsJsonPartFilter.class);
243
244    /** JSON key for the list of part keys. */
245    public static final String KEY_PARTS = "parts";
246
247    /** Name of the parameter used to enable JSON rendering. */
248    public static final String PARAM_JSON = "__json";
249
250    /** ThreadLocal used to detect nested calls. */
251    private ThreadLocal<Boolean> m_isNested = new ThreadLocal<Boolean>();
252
253    /**
254     * Detects whether the filter needs to be used for the given request.<p>
255     *
256     * @param request the request
257     * @return true if the filter should be used for the request
258     */
259    public static boolean isJsonRequest(ServletRequest request) {
260
261        HttpServletRequest sr = (HttpServletRequest)request;
262        return (sr.getQueryString() != null) && (sr.getQueryString().contains("__json=true"));
263    }
264
265    /**
266     * @see javax.servlet.Filter#destroy()
267     */
268    public void destroy() {
269        // do nothing
270    }
271
272    /**
273     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
274     */
275    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
276    throws IOException, ServletException {
277
278        if (isJsonRequest(request)) {
279            if (m_isNested.get() == null) {
280                try {
281                    m_isNested.set(Boolean.TRUE);
282                    RequestWrapper reqWrapper = new RequestWrapper((HttpServletRequest)request);
283                    ResponseWrapper resWrapper = new ResponseWrapper((HttpServletResponse)response);
284                    chain.doFilter(reqWrapper, resWrapper);
285                    byte[] data = resWrapper.getBytes();
286                    String content = new String(data, resWrapper.getCharacterEncoding());
287                    String transformedContent = transformContent(content);
288                    byte[] transformedData = transformedContent.getBytes("UTF-8");
289                    response.setContentType("application/json; charset=UTF-8");
290                    response.setContentLength(transformedData.length);
291                    response.getOutputStream().write(transformedData);
292                    response.getOutputStream().flush();
293                } finally {
294                    m_isNested.set(null);
295                }
296            }
297        } else {
298            chain.doFilter(request, response);
299        }
300    }
301
302    /**
303     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
304     */
305    public void init(FilterConfig filterConfig) {
306        // do nothing
307    }
308
309    /**
310     * Transforms the response content as a string.<p>
311     *
312     * @param content the content
313     * @return the transformed content
314     */
315    private String transformContent(String content) {
316
317        try {
318            List<CmsJsonPart> parts = CmsJsonPart.parseJsonParts(content);
319            JSONArray keys = new JSONArray();
320            JSONObject output = new JSONObject();
321            for (CmsJsonPart part : parts) {
322                if (output.has(part.getKey())) {
323                    LOG.warn("Duplicate key for JSON parts: " + part.getKey());
324                }
325                keys.put(part.getKey());
326                output.put(part.getKey(), part.getValue());
327            }
328            output.put(KEY_PARTS, keys);
329            return output.toString();
330        } catch (Exception e) {
331            LOG.error(e.getLocalizedMessage(), e);
332            return content;
333
334        }
335    }
336}