View Javadoc
1   /**
2    * This file Copyright (c) 2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.rest.client.app.ui;
35  
36  import info.magnolia.context.Context;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.i18nsystem.SimpleTranslator;
39  import info.magnolia.resteasy.client.RestEasyClient;
40  
41  import java.io.EOFException;
42  import java.io.IOException;
43  import java.io.InputStream;
44  import java.lang.annotation.Annotation;
45  import java.lang.reflect.InvocationTargetException;
46  import java.lang.reflect.Method;
47  import java.net.URLDecoder;
48  import java.util.ArrayList;
49  import java.util.Arrays;
50  import java.util.List;
51  import java.util.Map;
52  
53  import javax.inject.Inject;
54  import javax.jcr.RepositoryException;
55  
56  import org.apache.commons.lang3.ClassUtils;
57  import org.apache.commons.lang3.StringUtils;
58  import org.codehaus.jackson.JsonNode;
59  import org.codehaus.jackson.map.ObjectMapper;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import com.vaadin.data.util.ObjectProperty;
64  import com.vaadin.data.util.PropertysetItem;
65  import com.vaadin.server.Page;
66  import com.vaadin.server.StreamResource;
67  
68  /**
69   * RestClientApp presenter.
70   */
71  public class RestClientAppPresenter implements RestClientAppView.Listener {
72  
73      protected static final String RESPONSE = "Response";
74      protected static final String PARAMETERS = "Parameters";
75      protected static final String REQUEST_BODY = "Body";
76      protected static final String REST_CLIENT = "Client";
77      protected static final String REST_SERVICE = "Service";
78      protected static final String SERVICE_METHOD = "Method";
79      protected static final String METHOD_ANNOTATIONS = "Annotations";
80  
81      private static final Logger log = LoggerFactory.getLogger(RestClientAppPresenter.class);
82  
83      private final RestClientAppView view;
84      private final SimpleTranslator i18n;
85  
86      private ObjectMapper mapper = new ObjectMapper();
87      private PropertysetItem item;
88  
89      @Inject
90      public RestClientAppPresenter(RestClientAppView view, SimpleTranslator i18n) {
91          this.view = view;
92          this.i18n = i18n;
93      }
94  
95      public RestClientAppView start() {
96          view.setListener(this);
97          try {
98              // feed the view
99              view.setDataSource(createDataSource());
100         } catch (RepositoryException e) {
101             log.warn("An error occured while creating the app view", e);
102         }
103         return view;
104     }
105 
106     @Override
107     public String onRequest(RestEasyClient client, String serviceClass, final Method methodInstance, final String parameters, final String body, String contextAttributes) throws Throwable {
108         setMgnlContextParams(contextAttributes);
109         final Object service = client.getClientService(this.getClass(serviceClass));
110         Object result = MgnlContext.doInSystemContext(new MgnlContext.Op<Object, Throwable>() {
111             @Override
112             public Object exec() throws Throwable {
113                 try {
114                     return methodInstance.invoke(service, parseParameters(methodInstance, parameters, body));
115                 } catch (InvocationTargetException e) {
116                     throw e.getTargetException();
117                 }
118             }
119         });
120         return processOutput(result);
121     }
122 
123     @Override
124     public String onBodyFormat(Method methodValue, String body) throws IOException {
125         if (StringUtils.isBlank(body) || methodValue == null) {
126             return body;
127         }
128         Class bodyClass = this.getBodyClass(methodValue.getParameterTypes(), methodValue.getParameterAnnotations());
129         if (bodyClass != null && JsonNode.class.isAssignableFrom(bodyClass)) {
130             return body;
131         }
132         return body;
133     }
134 
135     @Override
136     public String[] onMethodChange(Method method) {
137         final StringBuilder methodDescription = new StringBuilder();
138         final StringBuilder paramsNames = new StringBuilder();
139 
140         if (method != null) {
141             for (Annotation annotation : method.getAnnotations()) {
142                 methodDescription.append(annotation);
143                 methodDescription.append("\n");
144             }
145             methodDescription.append(i18n.translate("restclient.app.parameters.methodParameter"));
146 
147             Annotation[][] annotations = method.getParameterAnnotations();
148             Class[] methodParameters = method.getParameterTypes();
149             method.getTypeParameters();
150             for (int i = 0; i < methodParameters.length; i++) {
151                 methodDescription.append(i + 1);
152                 methodDescription.append(". ");
153                 if (annotations[i].length == 0) {
154                     methodDescription.append(i18n.translate("restclient.app.parameters.body"));
155                 } else {
156                     methodDescription.append(Arrays.asList(annotations[i]));
157                     paramsNames.append(this.getAnnotationValue(annotations[i]));
158                     paramsNames.append("=\n");
159                 }
160                 methodDescription.append(" (");
161                 methodDescription.append(methodParameters[i].getSimpleName());
162                 methodDescription.append(")");
163                 methodDescription.append("\n");
164             }
165         }
166         final String paramsNamesStr = paramsNames.length() == 0 ? StringUtils.EMPTY : paramsNames.substring(0, paramsNames.length() - 1);
167         return new String[]{methodDescription.toString(), paramsNamesStr};
168     }
169 
170     @Override
171     public List<Method> onRestClientChange(String className) throws ClassNotFoundException {
172         if (StringUtils.isNotEmpty(className)) {
173             Class serviceClass = this.getClass(className);
174             return Arrays.asList(serviceClass.getMethods());
175         }
176         return null;
177     }
178 
179     private String getAnnotationValue(Annotation[] annotations) {
180         final StringBuilder value = new StringBuilder();
181         for (Annotation annotation : annotations) {
182             value.append(StringUtils.substringBetween(annotation.toString(), "=", ")"));
183         }
184         return value.toString();
185     }
186 
187     private Class getClass(String className) throws ClassNotFoundException {
188         return Class.forName(className);
189     }
190 
191     private Object[] parseParameters(Method method, String parametersAsString, String body) throws IOException {
192         List<Object> parameters = new ArrayList<Object>();
193 
194         if (StringUtils.isNotEmpty(parametersAsString)) {
195             Map<String, String> parametersMap = com.google.common.base.Splitter.on("\n").withKeyValueSeparator("=").split(StringUtils.trimToEmpty(parametersAsString));
196             Class[] types = method.getParameterTypes();
197             Annotation[][] annotations = method.getParameterAnnotations();
198             if (types.length != parametersMap.size() + (this.getBodyClass(types, annotations) != null ? 1 : 0)) {
199                 throw new IllegalArgumentException("Wrong number of arguments");
200             }
201             for (int i = 0; i < types.length; i++) {
202                 if (annotations[i].length == 0) { //body param
203                     parameters.add(corvertValue(body, types[i]));
204                 } else {
205                     parameters.add(corvertValue(parametersMap.get(this.getAnnotationValue(annotations[i])), types[i]));
206                 }
207             }
208         }
209         return parameters.toArray();
210     }
211 
212     private Class getBodyClass(Class[] types, Annotation[][] annotations) {
213         for (int i = 0; i < types.length; i++) {
214             if (annotations[i].length == 0) { //body param
215                 return types[i];
216             }
217         }
218         return null;
219     }
220 
221     protected Object corvertValue(String input, Class clazz) throws IOException {
222         if (StringUtils.EMPTY.equals(input)) { //allow to define null values
223             return null;
224         }
225         Object convertedValue = null;
226         Class wrapperClass = clazz;
227         if (clazz.isPrimitive()) {
228             wrapperClass = ClassUtils.primitiveToWrapper(clazz);
229         }
230 
231         try {
232             if (JsonNode.class.isAssignableFrom(wrapperClass)) {
233                 convertedValue = mapper.readTree(input);
234             } else {
235                 convertedValue = wrapperClass.getConstructor(String.class).newInstance(input);
236             }
237         } catch (EOFException e) {
238             throw new IllegalArgumentException(String.format("Illegal value '%s' for class '%s'.", input, wrapperClass), e);
239         } catch (Exception e) {
240             throw new IllegalArgumentException(String.format("Don't know how to convert '%s' to an instance of '%s'. " +
241                     "Override method '%s' if you need to handle this type.", input, clazz, "info.magnolia.rest.client.app.RestClientAppViewImpl.corvertValue"), e);
242         }
243         if (clazz.isPrimitive()) {
244             return convertedValue.hashCode(); //hashCode = primitive value
245         }
246         return convertedValue;
247     }
248 
249     protected String processOutput(Object output) throws IOException {
250         String result = String.valueOf(output);
251         if (output instanceof InputStream) {
252             this.download("restCallResponse", StringUtils.EMPTY, (InputStream) output);
253         }
254         return result;
255     }
256 
257     private void setMgnlContextParams(String params) {
258         if (StringUtils.isBlank(params)) {
259             return;
260         }
261         Map<String, String> parametersMap = com.google.common.base.Splitter.on("\n").withKeyValueSeparator("=").split(StringUtils.trimToEmpty(params));
262         for (Map.Entry<String, String> entry : parametersMap.entrySet()) {
263             String value = StringUtils.isEmpty(entry.getValue()) ? null : URLDecoder.decode(entry.getValue());
264             MgnlContext.setAttribute(entry.getKey(), value, Context.SESSION_SCOPE);
265         }
266     }
267 
268     private PropertysetItem createDataSource() throws RepositoryException {
269         item = new PropertysetItem();
270         item.addItemProperty(REST_CLIENT, new ObjectProperty<RestEasyClient>(null, RestEasyClient.class));
271         item.addItemProperty(REST_SERVICE, new ObjectProperty<String>(StringUtils.EMPTY));
272         item.addItemProperty(METHOD_ANNOTATIONS, new ObjectProperty<String>(StringUtils.EMPTY));
273         item.addItemProperty(SERVICE_METHOD, new ObjectProperty<Method>(null, Method.class));
274         item.addItemProperty(PARAMETERS, new ObjectProperty<String>(StringUtils.EMPTY));
275         item.addItemProperty(REQUEST_BODY, new ObjectProperty<String>(StringUtils.EMPTY));
276         item.addItemProperty(RESPONSE, new ObjectProperty<String>(StringUtils.EMPTY));
277         return item;
278     }
279 
280     private void download(String fileName, String mimeType, final InputStream output) {
281         StreamResource.StreamSource source = new StreamResource.StreamSource() {
282             @Override
283             public InputStream getStream() {
284                 return output;
285             }
286         };
287         StreamResource resource = new StreamResource(source, fileName);
288         // Accessing the DownloadStream via getStream() will set its cacheTime to whatever is set in the parent
289         // StreamResource. By default it is set to 1000 * 60 * 60 * 24, thus we have to override it beforehand.
290         // A negative value or zero will disable caching of this stream.
291         resource.setCacheTime(-1);
292         resource.getStream().setParameter("Content-Disposition", "attachment; filename=" + fileName + "\"");
293         resource.setMIMEType(mimeType);
294         Page.getCurrent().open(resource, StringUtils.EMPTY, true);
295     }
296 
297 }