Статьи

Быстрая и немного грязная генерация JSON-схемы с MOXy 2.5.1

Поэтому в настоящее время я работаю над новым REST API для будущей облачной службы Oracle, поэтому одной из вещей, которые мне понадобились, была возможность автоматически генерировать схему JSON для компонента в моей модели. Я использую MOXy для генерации JSON из POJO, и с версии 2.5.1 EclipseLink теперь у него есть возможность генерировать JSON-схему из модели бина.

В будущем будет более формальное решение, интегрированное в Jersey 2.x; но это решение подойдет на данный момент, если вы хотите поиграть с этим.

Итак, первый класс, который мы должны установить, это модель процессора, в основном внутреннего класса Джерси, которая позволяет нам дополнять модель ресурсов дополнительными методами и ресурсами. К каждому ресурсу в модели мы можем добавить JsonSchemaHandler который выполняет тяжелую работу по созданию новой схемы. Поскольку это простой POC, здесь кеширование не происходит, учтите это, если собираетесь использовать это в рабочем коде.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import com.google.common.collect.Lists;
 
import example.Bean;
 
import java.io.IOException;
import java.io.StringWriter;
 
import java.text.SimpleDateFormat;
 
import java.util.Date;
import java.util.List;
 
import javax.inject.Inject;
 
import javax.ws.rs.HttpMethod;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import javax.xml.bind.JAXBException;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
 
import org.eclipse.persistence.jaxb.JAXBContext;
 
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.server.model.RuntimeResource;
import org.glassfish.jersey.server.model.internal.ModelProcessorUtil;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
 
public class JsonSchemaModelProcessor implements ModelProcessor {
 
  private static final MediaType JSON_SCHEMA_TYPE =
    MediaType.valueOf("application/schema+json");
  private final List<ModelProcessorUtil.Method> methodList;
 
 
  public JsonSchemaModelProcessor() {
    methodList = Lists.newArrayList();
    methodList.add(new ModelProcessorUtil.Method("$schema", HttpMethod.GET,
      MediaType.WILDCARD_TYPE, JSON_SCHEMA_TYPE,
      JsonSchemaHandler.class));
  }
 
  @Override
  public ResourceModel processResourceModel(ResourceModel resourceModel,
      Configuration configuration) {
    return ModelProcessorUtil.enhanceResourceModel(resourceModel, true, methodList,
      true).build();
  }
 
  @Override
  public ResourceModel processSubResource(ResourceModel resourceModel,
      Configuration configuration) {
    return ModelProcessorUtil.enhanceResourceModel(resourceModel, true, methodList,
      true).build();
  }
 
 
  public static class JsonSchemaHandler
    implements Inflector<ContainerRequestContext, Response> {
 
    private final String lastModified = new SimpleDateFormat(WadlResource.HTTPDATEFORMAT).format(new Date());
 
    @Inject
    private ExtendedUriInfo extendedUriInfo;
 
    @Override
    public Response apply(ContainerRequestContext containerRequestContext) {
 
      // Find the resource that we are decorating, then work out the
      // return type on the first GET
 
      List<RuntimeResource> ms = extendedUriInfo.getMatchedRuntimeResources();
      List<ResourceMethod> rms = ms.get(1).getResourceMethods();
      Class responseType = null;
      found:
      for (ResourceMethod rm : rms) {
        if ("GET".equals(rm.getHttpMethod())) {
          responseType = (Class) rm.getInvocable().getResponseType();
          break found;
        }
      }
 
      if (responseType == null) {
        throw new WebApplicationException("Cannot resolve type for schema generation");
      }
 
      //
      try {
        JAXBContext context = (JAXBContext) JAXBContext.newInstance(responseType);
 
        StringWriter sw = new StringWriter();
        final StreamResult sr = new StreamResult(sw);
 
        context.generateJsonSchema(new SchemaOutputResolver() {
          @Override
          public Result createOutput(String namespaceUri, String suggestedFileName)
              throws IOException {
            return sr;
          }
        }, responseType);
 
 
        return Response.ok().type(JSON_SCHEMA_TYPE)
          .header("Last-modified", lastModified)
          .entity(sw.toString()).build();
      } catch (JAXBException jaxb) {
        throw new WebApplicationException(jaxb);
      }
    }
  }
 
 
}

Обратите внимание на очень простую эвристику в коде JsonSchemaHandler предполагающую, что для каждого ресурса существует сопоставление 1: 1 с одним элементом схемы JSON. Это, конечно, может быть не так для вашего конкретного приложения.

Теперь, когда у нас есть схема, сгенерированная в известном месте, нам нужно сообщить об этом клиенту, первое, что мы сделаем, это убедитесь, что существует подходящий заголовок ссылки, когда пользователь вызывает OPTIONS для определенного ресурса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.IOException;
 
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.UriInfo;
 
public class JsonSchemaResponseFilter implements ContainerResponseFilter {
 
  @Context
  private UriInfo uriInfo;
 
  @Override
  public void filter(ContainerRequestContext containerRequestContext,
                     ContainerResponseContext containerResponseContext) throws IOException {
 
 
    String method = containerRequestContext.getMethod();
    if ("OPTIONS".equals(method)) {
 
      Link schemaUriLink =
        Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
          .path("$schema")).rel("describedBy").build();
 
      containerResponseContext.getHeaders().add("Link", schemaUriLink);
    }
  }
}

Поскольку это JAX-RS 2.x, с которым мы работаем, мы, конечно же, собираемся объединить все вместе в одну функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
 
public class JsonSchemaFeature implements Feature {
 
  @Override
  public boolean configure(FeatureContext featureContext) {
 
    if (!featureContext.getConfiguration().isRegistered(JsonSchemaModelProcessor.class)) {
      featureContext.register(JsonSchemaModelProcessor.class);
      featureContext.register(JsonSchemaResponseFilter.class);
      return true;
    }
    return false;
  }
}

Я не собираюсь показывать весь мой набор классов POJO; но очень быстро это класс Resource с методом @GET, необходимым для кода генерации схемы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
 
@Path("/bean")
public class BeanResource {
 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Bean getBean() {
        return new Bean();
    }
}

И, наконец, вот что вы видите, если выполняете GET для ресурса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
GET .../resources/bean
Content-Type: application/json
 
{
  "message" : "hello",
  "other" : {
    "message" : "OtherBean"
  },
  "strings" : [
    "one",
    "two",
    "three",
    "four"
  ]
}

И ВАРИАНТЫ:

1
2
3
4
5
OPTIONS .../resources/bean
Content-Type: text/plain
Link: <http://.../resources/bean/$schema>; rel="describedBy"
 
GET, OPTIONS, HEAD

И, наконец, если вы разрешите ресурс схемы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
GET .../resources/bean/$schema
Content-Type: application/schema+json
 
{
  "title" : "example.Bean",
  "type" : "object",
  "properties" : {
    "message" : {
      "type" : "string"
    },
    "other" : {
      "$ref" : "#/definitions/OtherBean"
    },
    "strings" : {
      "type" : "array",
      "items" : {
        "type" : "string"
      }
    }
  },
  "additionalProperties" : false,
  "definitions" : {
    "OtherBean" : {
      "type" : "object",
      "properties" : {
        "message" : {
          "type" : "string"
        }
      },
      "additionalProperties" : false
    }
  }
}

Здесь предстоит проделать немалую работу, в частности, создание гипермедиа-расширений на основе декларативных аннотаций ссылок, которые я пересылаю, портировал в Jersey 2.x некоторое время назад. Но это действительно указывает на решение, и мы можем использовать различные решения, чтобы что-то работать сейчас.