001    /*
002     * Licensed under the Apache License, Version 2.0 (the "License");
003     * you may not use this file except in compliance with the License.
004     * You may obtain a copy of the License at
005     *
006     * http://www.apache.org/licenses/LICENSE-2.0
007     *
008     * Unless required by applicable law or agreed to in writing, software
009     * distributed under the License is distributed on an "AS IS" BASIS,
010     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011     * See the License for the specific language governing permissions and
012     * limitations under the License.
013     * 
014     * See the NOTICE file distributed with this work for additional
015     * information regarding copyright ownership.
016     */
017    
018    package com.osbcp.apbs;
019    
020    import java.io.IOException;
021    import java.io.Serializable;
022    import java.lang.reflect.Constructor;
023    import java.util.Map;
024    
025    import javax.servlet.ServletException;
026    import javax.servlet.http.HttpServlet;
027    import javax.servlet.http.HttpServletRequest;
028    import javax.servlet.http.HttpServletResponse;
029    
030    import com.osbcp.apbs.events.AjaxPostBackEvent;
031    import com.osbcp.jjsw.JavaScript;
032    
033    /**
034     * A specific HttpServlet for AjaxPostBack.
035     * 
036     * @author <a href="mailto:christoffer@christoffer.me">Christoffer Pettersson</a>
037     */
038    
039    public abstract class AjaxPostBackServlet extends HttpServlet {
040    
041            private static final long serialVersionUID = 1L;
042    
043            @Override
044            public final void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
045    
046                    // Get the data from the query string
047                    String eventName = request.getParameter("event");
048                    String[] arguments = request.getParameterValues("argument");
049    
050                    // Populate the meta data map
051                    Map<Serializable, Object> metaData = getMetaData(request, response);
052    
053                    // Get the response script
054                    JavaScript responseScript = getJavaScriptResponse(eventName, arguments, metaData);
055    
056                    // Print out the JavaScript 
057                    printJavaScriptResponse(response, responseScript);
058    
059            }
060    
061            /**
062             * Logic that invokes the given event with the specified arguments and meta data.
063             * 
064             * @param eventName The class name of the event to be triggered.
065             * @param arguments The constructor arguments to the event.
066             * @param metaData Any meta data that should be included in the event. 
067             * @return A JavaScript response that should be performed by the browser.
068             */
069    
070            @SuppressWarnings("unchecked")
071            private JavaScript getJavaScriptResponse(final String eventName, final String[] arguments, final Map<Serializable, Object> metaData) {
072    
073                    // Create a new response container
074                    JavaScript response = new JavaScript();
075    
076                    // Save some variables for logging
077                    long startTime = System.currentTimeMillis();
078    
079                    try {
080    
081                            // Get the event class
082                            final Class<? extends AjaxPostBackEvent> eventClass = (Class<? extends AjaxPostBackEvent>) Class.forName(eventName);
083    
084                            // Get a new instance of the event
085                            Object event = invokeEvent(eventClass, arguments);
086    
087                            // Cast it as an AjaxPostBackEvent
088                            AjaxPostBackEvent apb = (AjaxPostBackEvent) event;
089    
090                            // Set any meta data on the event
091                            apb.setMetaData(metaData);
092    
093                            // Get the response from the event
094                            response.append(apb.getJavaScriptResponse());
095    
096                    } catch (final Exception exception) {
097    
098                            // Calculate the duration
099                            long duration = System.currentTimeMillis() - startTime;
100    
101                            // Bubble the error
102                            onError(eventName, arguments, duration, response);
103    
104                            // Get the response if any exception occurs
105                            return getExceptionJavaScriptResponse(eventName, arguments, duration, exception);
106    
107                    }
108    
109                    // Calculate the duration
110                    long duration = System.currentTimeMillis() - startTime;
111    
112                    // Bubble the success
113                    onSuccess(eventName, arguments, duration, response);
114    
115                    // Return the response
116                    return response;
117    
118            }
119    
120            /**
121             * Invokes a specific event with specific arguments.
122             * 
123             * @param eventClass The class of the event to be triggered.
124             * @param arguments The constructor arguments to the event.
125             * @return An instance of the event.
126             * @throws Exception If any error occurs.
127             */
128    
129            private Object invokeEvent(final Class<? extends AjaxPostBackEvent> eventClass, final String[] arguments) throws Exception {
130    
131                    if (arguments != null) {
132    
133                            Constructor<?>[] constructors = eventClass.getConstructors();
134    
135                            for_each_constructor: for (Constructor<?> constructor : constructors) {
136    
137                                    Class<?>[] constructorTypes = constructor.getParameterTypes();
138    
139                                    // If they don't have the same number of slots, continue to the next constructor
140                                    if (constructorTypes.length != arguments.length) {
141                                            continue;
142                                    }
143    
144                                    // Create an array for the arguments we are going to type cast
145                                    Object[] castedArguments = new Object[constructorTypes.length];
146    
147                                    for (int i = 0; i < arguments.length; i++) {
148    
149                                            try {
150    
151                                                    // Try and cast the argument to what the constructor expects
152                                                    castedArguments[i] = cast(arguments[i], constructorTypes[i]);
153    
154                                            } catch (ClassCastException exception) {
155    
156                                                    // If there is a class cast exception, this is the incorrect constructor
157                                                    continue for_each_constructor;
158    
159                                            }
160    
161                                    }
162    
163                                    // Invoke the event using the newly casted arguments
164                                    return constructor.newInstance(castedArguments);
165    
166                            }
167    
168                            throw new Exception("Could not find any suitable constructor for the class '" + eventClass.getSimpleName() + "' for the given arguments " + arguments + ".");
169    
170                    } else {
171    
172                            // Create an instance of the event with the default empty constructor
173                            return eventClass.newInstance();
174    
175                    }
176    
177            }
178    
179            /**
180             * Casts a specific argument to a specific legal type.
181             * 
182             * @param argument The argument that should be casted.
183             * @param castTo The type the argument should be casted to.
184             * @return The argument casted to a legal type.
185             * @throws ClassCastException If any error occurs.
186             */
187    
188            private Object cast(final String argument, final Class<?> castTo) throws ClassCastException {
189    
190                    try {
191    
192                            if (castTo == String.class) {
193                                    return argument;
194                            } else if (castTo == Byte.class || castTo == Byte.TYPE) {
195                                    return Byte.parseByte(argument);
196                            } else if (castTo == Short.class || castTo == Short.TYPE) {
197                                    return Short.parseShort(argument);
198                            } else if (castTo == Integer.class || castTo == Integer.TYPE) {
199                                    return Integer.parseInt(argument);
200                            } else if (castTo == Long.class || castTo == Long.TYPE) {
201                                    return Long.parseLong(argument);
202                            } else if (castTo == Float.class || castTo == Float.TYPE) {
203                                    return Float.parseFloat(argument);
204                            } else if (castTo == Double.class || castTo == Double.TYPE) {
205                                    return Double.parseDouble(argument);
206                            } else if (castTo == Boolean.class || castTo == Boolean.TYPE) {
207                                    return Boolean.valueOf(argument);
208                            } else if (castTo == Character.class || castTo == Character.TYPE) {
209                                    return argument.charAt(0);
210                            }
211    
212                    } catch (Exception e) {
213                            // Fall through
214                    }
215    
216                    throw new ClassCastException("The argument '" + argument + "' could not be casted to anything legal.");
217    
218            }
219    
220            /**
221             * Writes a specific JavaScript script to the HTTP response stream.
222             * 
223             * @param response The HTTP response.
224             * @param responseScript The JavaScript script that should be written.
225             * @throws IOException If any error occurs.
226             */
227    
228            private void printJavaScriptResponse(final HttpServletResponse response, final JavaScript responseScript) throws IOException {
229    
230                    response.setCharacterEncoding("UTF-8");
231                    response.setContentType("text/javascript");
232                    response.getWriter().println(responseScript);
233    
234            }
235    
236            /**
237             * Used to specific specific meta data that should be included in AjaxPostBackEvents.
238             * 
239             * @param request The HTTP request.
240             * @param response The HTTP response.
241             * @return A map of meta data that should be included in AjaxPostBackEvents.
242             */
243    
244            protected abstract Map<Serializable, Object> getMetaData(final HttpServletRequest request, final HttpServletResponse response);
245    
246            /**
247             * Returns the JavaScript script that should be sent to the browser if any exception error occurs.
248             * 
249             * @param eventName The class name of the event.
250             * @param arguments The arguments sent to the event.
251             * @param duration The duration in milliseconds of the event logic.
252             * @param exception The exception error that occurred.
253             * @return The JavaScript script that should be sent to the browser if any exception error occurs.
254             */
255    
256            protected abstract JavaScript getExceptionJavaScriptResponse(final String eventName, final String[] arguments, final long duration, final Exception exception);
257    
258            /**
259             * Triggers when an event is successfully triggered.
260             * 
261             * @param eventName The class name of the event.
262             * @param arguments The arguments sent to the event.
263             * @param duration The duration in milliseconds of the event logic.
264             * @param response The JavaScript response the event returned.
265             */
266    
267            protected abstract void onSuccess(final String eventName, final String[] arguments, final long duration, final JavaScript response);
268    
269            /**
270             * Triggers when an event failed.
271             * 
272             * @param eventName The class name of the event.
273             * @param arguments The arguments sent to the event.
274             * @param duration The duration in milliseconds of the event logic.
275             * @param response The JavaScript response the event returned.
276             */
277    
278            protected abstract void onError(final String eventName, final String[] arguments, final long duration, final JavaScript response);
279    
280    }