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