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}