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}