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.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
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   // @formatter:off
064   @RestGet(
065      path="/",
066      summary="List of available remote interfaces",
067      description="Shows a list of the interfaces registered with this remote interface servlet."
068   )
069   // @formatter:on
070   public List<LinkString> getInterfaces() throws Exception {
071      var l = new LinkedList<LinkString>();
072      for (var c : getServiceMap().keySet())
073         l.add(new LinkString(c.getName(), "servlet:/{0}", urlEncode(c.getName())));
074      return l;
075   }
076
077   // @formatter:off
078   @RestPost(
079      path="/{javaInterface}/{javaMethod}",
080      summary="Invoke an interface method",
081      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.",
082      swagger=@OpSwagger(
083         parameters= {
084            "{",
085               "in: 'body',",
086               "description: 'Serialized array of Java objects',",
087               "schema: {",
088                  "type': 'array'",
089               "},",
090               "examples: {",
091                  "'application/json+lax': '[\\'foo\\', 123, true]'",
092               "}",
093            "}"
094         },
095         responses= {
096            "200:{ description:'The return object serialized', schema:{type:'any'},example:{foo:123} }",
097         }
098      )
099   )
100   @HtmlDocConfig(
101      nav= {
102         "<h5>Interface:  $RP{javaInterface}</h5>",
103         "<h5>Method:  $RP{javaMethod}</h5>"
104      }
105   )
106   public Object invoke(
107         Reader r,
108         ReaderParser p,
109         @Header("Content-Type") ContentType contentType,
110         @Path("javaInterface") @Schema(description="Java interface name") String javaInterface,
111         @Path("javaMethod") @Schema(description="Java method name") String javaMethod
112      ) throws UnsupportedMediaType, NotFound, Exception {
113      // @formatter:on
114
115      // Find the parser.
116      if (p == null)
117         throw new UnsupportedMediaType("Could not find parser for media type ''{0}''", contentType);
118      RrpcInterfaceMeta rim = getInterfaceClass(javaInterface);
119
120      // Find the service.
121      Object service = getServiceMap().get(rim.getJavaClass());
122      if (service == null)
123         throw new NotFound("Service not found");
124
125      // Find the method.
126      RrpcInterfaceMethodMeta rmm = getMethods(javaInterface).get(javaMethod);
127      if (rmm == null)
128         throw new NotFound("Method not found");
129
130      // Parse the args and invoke the method.
131      java.lang.reflect.Method m = rmm.getJavaMethod();
132      Object[] params = p.parseArgs(r, m.getGenericParameterTypes());
133      return m.invoke(service, params);
134   }
135
136   // @formatter:off
137   @RestGet(
138      path="/{javaInterface}",
139      summary="List of available methods on interface",
140      description="Shows a list of all the exposed methods on an interface."
141   )
142   @HtmlDocConfig(
143      nav="<h5>Interface:  $RP{javaInterface}</h5>"
144   )
145   // @formatter:on
146   public Collection<LinkString> listMethods(@Path("javaInterface") @Schema(description = "Java interface name") String javaInterface) throws Exception {
147
148      List<LinkString> l = list();
149      for (var s : getMethods(javaInterface).keySet())
150         l.add(new LinkString(s, "servlet:/{0}/{1}", urlEncode(javaInterface), urlEncode(s)));
151      return l;
152   }
153
154   // @formatter:off
155   @RestGet(
156      path="/{javaInterface}/{javaMethod}",
157      summary="Form entry for interface method call",
158      description="Shows a form entry page for executing a remote interface method."
159   )
160   @HtmlDocConfig(
161      nav={
162         "<h5>Interface:  $RP{javaInterface}</h5>",
163         "<h5>Method:  $RP{javaMethod}</h5>"
164      }
165   )
166   public Div showEntryForm(
167         @Path("javaInterface") @Schema(description="Java interface name") String javaInterface,
168         @Path("javaMethod") @Schema(description="Java method name") String javaMethod
169      ) throws NotFound, Exception {
170      // @formatter:on
171
172      // Find the method.
173      RrpcInterfaceMethodMeta rmm = getMethods(javaInterface).get(javaMethod);
174      if (rmm == null)
175         throw new NotFound("Method not found");
176
177      Table t = table();
178
179      Type[] types = rmm.getJavaMethod().getGenericParameterTypes();
180      if (types.length == 0) {
181         t.child(tr(td("No arguments").colspan(3).style("text-align:center")));
182      } else {
183         t.child(tr(th("Index"), th("Type"), th("Value")));
184         for (var i = 0; i < types.length; i++) {
185            String type = Mutaters.toString(types[i]);
186            t.child(tr(td(i), td(type), td(input().name(String.valueOf(i)).type("text"))));
187         }
188      }
189
190      t.child(tr(td().colspan(3).style("text-align:right").children(types.length == 0 ? null : button("reset", "Reset"), button("button", "Cancel").onclick("window.location.href='/'"),
191         button("submit", "Submit"))));
192
193      return div(form().id("form").action("request:/").method(POST).children(t));
194   }
195
196   /**
197    * Return the <c>Class</c> given it's name if it exists in the services map.
198    */
199   private RrpcInterfaceMeta getInterfaceClass(String javaInterface) throws NotFound, Exception {
200      RrpcInterfaceMeta rm = serviceMap.get(javaInterface);
201      if (rm == null) {
202         for (var c : getServiceMap().keySet()) {
203            if (c.getName().equals(javaInterface)) {
204               rm = new RrpcInterfaceMeta(c, null);
205               serviceMap.put(javaInterface, rm);
206               return rm;
207            }
208         }
209         throw new NotFound("Interface class not found");
210      }
211      return rm;
212   }
213
214   private Map<String,RrpcInterfaceMethodMeta> getMethods(String javaInterface) throws Exception {
215      return getInterfaceClass(javaInterface).getMethodsByPath();
216   }
217
218   /**
219    * Returns the list of interfaces to their implementation objects.
220    *
221    * <p>
222    * This class is called often and not cached, so any caching should occur in the subclass if necessary.
223    *
224    * @return The service map.
225    * @throws Exception Any exception.
226    */
227   protected abstract Map<Class<?>,Object> getServiceMap() throws Exception;
228}