blob: c28b4878bfb99667fea430920bb08348b535e475 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.apache.hugegraph.api.profile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.lang3.StringUtils;
import org.apache.tinkerpop.shaded.jackson.annotation.JsonProperty;
import org.glassfish.jersey.model.Parameter.Source;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.InsertionOrderUtil;
import org.apache.hugegraph.util.JsonUtil;
import org.apache.hugegraph.version.CoreVersion;
import com.codahale.metrics.annotation.Timed;
@Path("/")
@Singleton
@Tag(name = "ProfileAPI")
public class ProfileAPI {
private static final String SERVICE = "hugegraph";
private static final String DOC = "https://hugegraph.apache.org/docs/";
private static final String API_DOC = DOC + "clients/";
private static String SERVER_PROFILES = null;
private static String API_PROFILES = null;
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
public String getProfile(@Context Application application) {
// May init multi times by multi threads, but no effect on the results
if (SERVER_PROFILES != null) {
return SERVER_PROFILES;
}
Map<String, Object> profiles = InsertionOrderUtil.newMap();
profiles.put("service", SERVICE);
profiles.put("version", CoreVersion.VERSION.toString());
profiles.put("doc", DOC);
profiles.put("api_doc", API_DOC);
Set<String> apis = new TreeSet<>();
for (Class<?> clazz : application.getClasses()) {
if (!isAnnotatedPathClass(clazz)) {
continue;
}
Resource resource = Resource.from(clazz);
APICategory apiCategory = APICategory.parse(resource.getName());
apis.add(apiCategory.dir);
}
profiles.put("apis", apis);
SERVER_PROFILES = JsonUtil.toJson(profiles);
return SERVER_PROFILES;
}
@GET
@Path("apis")
@Timed
@Produces(MediaType.APPLICATION_JSON)
public String showAllAPIs(@Context Application application) {
if (API_PROFILES != null) {
return API_PROFILES;
}
APIProfiles apiProfiles = new APIProfiles();
for (Class<?> clazz : application.getClasses()) {
if (!isAnnotatedPathClass(clazz)) {
continue;
}
Resource resource = Resource.from(clazz);
APICategory apiCategory = APICategory.parse(resource.getName());
String url = resource.getPath();
// List all methods of this resource
for (ResourceMethod rm : resource.getResourceMethods()) {
APIProfile profile = APIProfile.parse(url, rm);
apiProfiles.put(apiCategory, profile);
}
// List all methods of this resource's child resources
for (Resource childResource : resource.getChildResources()) {
String childUrl = url + "/" + childResource.getPath();
for (ResourceMethod rm : childResource.getResourceMethods()) {
APIProfile profile = APIProfile.parse(childUrl, rm);
apiProfiles.put(apiCategory, profile);
}
}
}
API_PROFILES = JsonUtil.toJson(apiProfiles);
return API_PROFILES;
}
private static boolean isAnnotatedPathClass(Class<?> clazz) {
if (clazz.isAnnotationPresent(Path.class)) {
return true;
}
for (Class<?> i : clazz.getInterfaces()) {
if (i.isAnnotationPresent(Path.class)) {
return true;
}
}
return false;
}
private static class APIProfiles {
@JsonProperty("apis")
private final Map<String, Map<String, List<APIProfile>>> apis;
public APIProfiles() {
this.apis = new TreeMap<>();
}
public void put(APICategory category, APIProfile profile) {
Map<String, List<APIProfile>> categories;
categories = this.apis.computeIfAbsent(category.dir,
k -> new TreeMap<>());
List<APIProfile> profiles = categories.computeIfAbsent(
category.category,
k -> new ArrayList<>());
profiles.add(profile);
}
}
private static class APIProfile {
@JsonProperty("url")
private final String url;
@JsonProperty("method")
private final String method;
@JsonProperty("parameters")
private final List<ParamInfo> parameters;
public APIProfile(String url, String method,
List<ParamInfo> parameters) {
this.url = url;
this.method = method;
this.parameters = parameters;
}
public static APIProfile parse(String url, ResourceMethod resource) {
String method = resource.getHttpMethod();
List<ParamInfo> params = new ArrayList<>();
for (Parameter param : resource.getInvocable().getParameters()) {
if (param.getSource() == Source.QUERY) {
String name = param.getSourceName();
String type = param.getType().getTypeName();
String defaultValue = param.getDefaultValue();
params.add(new ParamInfo(name, type, defaultValue));
} else if (param.getSource() == Source.ENTITY) {
String type = param.getType().getTypeName();
params.add(new ParamInfo("body", type));
}
}
return new APIProfile(url, method, params);
}
private static class ParamInfo {
@JsonProperty("name")
private final String name;
@JsonProperty("type")
private final String type;
@JsonProperty("default_value")
private final String defaultValue;
public ParamInfo(String name, String type) {
this(name, type, null);
}
public ParamInfo(String name, String type, String defaultValue) {
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
}
}
}
private static class APICategory {
private final String dir;
private final String category;
public APICategory(String dir, String category) {
this.dir = dir;
this.category = category;
}
public static APICategory parse(String fullName) {
String[] parts = StringUtils.split(fullName, ".");
E.checkState(parts.length >= 2, "Invalid api name");
String dir = parts[parts.length - 2];
String category = parts[parts.length - 1];
return new APICategory(dir, category);
}
}
}