001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.remote;
018
019import static org.apache.juneau.bean.html5.HtmlBuilder.*;
020import static org.apache.juneau.common.utils.StringUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022import static org.apache.juneau.http.HttpMethod.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027import java.util.Map;
028import java.util.concurrent.*;
029
030import org.apache.juneau.annotation.*;
031import org.apache.juneau.bean.*;
032import org.apache.juneau.bean.html5.*;
033import org.apache.juneau.html.annotation.*;
034import org.apache.juneau.http.annotation.*;
035import org.apache.juneau.http.annotation.Header;
036import org.apache.juneau.http.header.*;
037import org.apache.juneau.http.remote.*;
038import org.apache.juneau.http.response.*;
039import org.apache.juneau.parser.*;
040import org.apache.juneau.reflect.*;
041import org.apache.juneau.rest.annotation.*;
042import org.apache.juneau.rest.servlet.*;
043
044/**
045 * Abstract class for defining Remote Interface Services.
046 *
047 * <p>
048 * Remote Interface Services are POJOs whose methods can be invoked remotely through proxy interfaces.
049 *
050 * <p>
051 * To implement a remote interface service, developers must simply subclass from this class and implement the
052 * {@link #getServiceMap()} method that maps java interfaces to POJO instances.
053 *
054 * <h5 class='section'>See Also:</h5><ul>
055 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestRpc">REST/RPC</a>
056 * </ul>
057 */
058@SuppressWarnings({"serial","javadoc"})
059public abstract class RrpcServlet extends BasicRestServlet {
060
061   private final Map<String,RrpcInterfaceMeta> serviceMap = new ConcurrentHashMap<>();
062
063   //-----------------------------------------------------------------------------------------------------------------
064   // Abstract methods
065   //-----------------------------------------------------------------------------------------------------------------
066
067   /**
068    * Returns the list of interfaces to their implementation objects.
069    *
070    * <p>
071    * This class is called often and not cached, so any caching should occur in the subclass if necessary.
072    *
073    * @return The service map.
074    * @throws Exception Any exception.
075    */
076   protected abstract Map<Class<?>,Object> getServiceMap() throws Exception;
077
078   //-----------------------------------------------------------------------------------------------------------------
079   // REST methods
080   //-----------------------------------------------------------------------------------------------------------------
081
082   @RestGet(
083      path="/",
084      summary="List of available remote interfaces",
085      description="Shows a list of the interfaces registered with this remote interface servlet."
086   )
087   public List<LinkString> getInterfaces() throws Exception {
088      List<LinkString> l = new LinkedList<>();
089      for (Class<?> c : getServiceMap().keySet())
090         l.add(new LinkString(c.getName(), "servlet:/{0}", urlEncode(c.getName())));
091      return l;
092   }
093
094   @RestGet(
095      path="/{javaInterface}",
096      summary="List of available methods on interface",
097      description="Shows a list of all the exposed methods on an interface."
098   )
099   @HtmlDocConfig(
100      nav="<h5>Interface:  $RP{javaInterface}</h5>"
101   )
102   public Collection<LinkString> listMethods(
103         @Path("javaInterface") @Schema(description="Java interface name") String javaInterface
104      ) throws Exception {
105
106      List<LinkString> l = list();
107      for (String s : getMethods(javaInterface).keySet())
108         l.add(new LinkString(s, "servlet:/{0}/{1}", urlEncode(javaInterface), urlEncode(s)));
109      return l;
110   }
111
112   @RestGet(
113      path="/{javaInterface}/{javaMethod}",
114      summary="Form entry for interface method call",
115      description="Shows a form entry page for executing a remote interface method."
116   )
117   @HtmlDocConfig(
118      nav={
119         "<h5>Interface:  $RP{javaInterface}</h5>",
120         "<h5>Method:  $RP{javaMethod}</h5>"
121      }
122   )
123   public Div showEntryForm(
124         @Path("javaInterface") @Schema(description="Java interface name") String javaInterface,
125         @Path("javaMethod") @Schema(description="Java method name") String javaMethod
126      ) throws NotFound, Exception {
127
128      // Find the method.
129      RrpcInterfaceMethodMeta rmm = getMethods(javaInterface).get(javaMethod);
130      if (rmm == null)
131         throw new NotFound("Method not found");
132
133      Table t = table();
134
135      Type[] types = rmm.getJavaMethod().getGenericParameterTypes();
136      if (types.length == 0) {
137         t.child(tr(td("No arguments").colspan(3).style("text-align:center")));
138      } else {
139         t.child(tr(th("Index"),th("Type"),th("Value")));
140         for (int i = 0; i < types.length; i++) {
141            String type = Mutaters.toString(types[i]);
142            t.child(tr(td(i), td(type), td(input().name(String.valueOf(i)).type("text"))));
143         }
144      }
145
146      t.child(
147         tr(
148            td().colspan(3).style("text-align:right").children(
149               types.length == 0 ? null : button("reset", "Reset"),
150               button("button","Cancel").onclick("window.location.href='/'"),
151               button("submit", "Submit")
152            )
153         )
154      );
155
156      return div(form().id("form").action("request:/").method(POST).children(t));
157   }
158
159   @RestPost(
160      path="/{javaInterface}/{javaMethod}",
161      summary="Invoke an interface method",
162      description="Invoke a Java method by passing in the arguments as an array of serialized objects.\nThe returned object is then serialized to the response.",
163      swagger=@OpSwagger(
164         parameters= {
165            "{",
166               "in: 'body',",
167               "description: 'Serialized array of Java objects',",
168               "schema: {",
169                  "type': 'array'",
170               "},",
171               "examples: {",
172                  "'application/json+lax': '[\\'foo\\', 123, true]'",
173               "}",
174            "}"
175         },
176         responses= {
177            "200:{ description:'The return object serialized', schema:{type:'any'},example:{foo:123} }",
178         }
179      )
180   )
181   @HtmlDocConfig(
182      nav= {
183         "<h5>Interface:  $RP{javaInterface}</h5>",
184         "<h5>Method:  $RP{javaMethod}</h5>"
185      }
186   )
187   public Object invoke(
188         Reader r,
189         ReaderParser p,
190         @Header("Content-Type") ContentType contentType,
191         @Path("javaInterface") @Schema(description="Java interface name") String javaInterface,
192         @Path("javaMethod") @Schema(description="Java method name") String javaMethod
193      ) throws UnsupportedMediaType, NotFound, Exception {
194
195      // Find the parser.
196      if (p == null)
197         throw new UnsupportedMediaType("Could not find parser for media type ''{0}''", contentType);
198      RrpcInterfaceMeta rim = getInterfaceClass(javaInterface);
199
200      // Find the service.
201      Object service = getServiceMap().get(rim.getJavaClass());
202      if (service == null)
203         throw new NotFound("Service not found");
204
205      // Find the method.
206      RrpcInterfaceMethodMeta rmm = getMethods(javaInterface).get(javaMethod);
207      if (rmm == null)
208         throw new NotFound("Method not found");
209
210      // Parse the args and invoke the method.
211      java.lang.reflect.Method m = rmm.getJavaMethod();
212      Object[] params = p.parseArgs(r, m.getGenericParameterTypes());
213      return m.invoke(service, params);
214   }
215
216
217   //-----------------------------------------------------------------------------------------------------------------
218   // Other methods
219   //-----------------------------------------------------------------------------------------------------------------
220
221   private Map<String,RrpcInterfaceMethodMeta> getMethods(String javaInterface) throws Exception {
222      return getInterfaceClass(javaInterface).getMethodsByPath();
223   }
224
225   /**
226    * Return the <c>Class</c> given it's name if it exists in the services map.
227    */
228   private RrpcInterfaceMeta getInterfaceClass(String javaInterface) throws NotFound, Exception {
229      RrpcInterfaceMeta rm = serviceMap.get(javaInterface);
230      if (rm == null) {
231         for (Class<?> c : getServiceMap().keySet()) {
232            if (c.getName().equals(javaInterface)) {
233               rm = new RrpcInterfaceMeta(c, null);
234               serviceMap.put(javaInterface, rm);
235               return rm;
236            }
237         }
238         throw new NotFound("Interface class not found");
239      }
240      return rm;
241   }
242}