| /* |
| * Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Dmitry Kornilov - Initial implementation |
| package org.eclipse.persistence.jpa.rs.resources; |
| |
| import static org.eclipse.persistence.jpa.rs.resources.common.AbstractResource.SERVICE_VERSION_FORMAT; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import jakarta.ws.rs.Consumes; |
| import jakarta.ws.rs.GET; |
| import jakarta.ws.rs.Path; |
| import jakarta.ws.rs.PathParam; |
| import jakarta.ws.rs.Produces; |
| import jakarta.ws.rs.core.Context; |
| import jakarta.ws.rs.core.HttpHeaders; |
| import jakarta.ws.rs.core.MediaType; |
| import jakarta.ws.rs.core.Response; |
| import jakarta.ws.rs.core.UriInfo; |
| import jakarta.xml.bind.JAXBException; |
| |
| import org.eclipse.persistence.descriptors.ClassDescriptor; |
| import org.eclipse.persistence.eis.mappings.EISCompositeCollectionMapping; |
| import org.eclipse.persistence.internal.expressions.ConstantExpression; |
| import org.eclipse.persistence.internal.expressions.MapEntryExpression; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.LinkV2; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.MetadataCatalog; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.Property; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.Reference; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.Resource; |
| import org.eclipse.persistence.internal.jpa.rs.metadata.model.v2.ResourceSchema; |
| import org.eclipse.persistence.internal.queries.ReportItem; |
| import org.eclipse.persistence.internal.sessions.AbstractSession; |
| import org.eclipse.persistence.jpa.rs.PersistenceContext; |
| import org.eclipse.persistence.jpa.rs.exceptions.JPARSException; |
| import org.eclipse.persistence.jpa.rs.features.ItemLinksBuilder; |
| import org.eclipse.persistence.jpa.rs.resources.common.AbstractResource; |
| import org.eclipse.persistence.jpa.rs.util.HrefHelper; |
| import org.eclipse.persistence.jpa.rs.util.JPARSLogger; |
| import org.eclipse.persistence.jpa.rs.util.StreamingOutputMarshaller; |
| import org.eclipse.persistence.mappings.CollectionMapping; |
| import org.eclipse.persistence.mappings.DatabaseMapping; |
| import org.eclipse.persistence.mappings.ForeignReferenceMapping; |
| import org.eclipse.persistence.queries.DatabaseQuery; |
| import org.eclipse.persistence.queries.ReportQuery; |
| import org.eclipse.persistence.sessions.DatabaseRecord; |
| |
| /** |
| * JPARS 2.0 metadata catalog. Resource metadata and schemas. |
| * |
| * @author Dmitry Kornilov |
| * @since EclipseLink 2.6.0. |
| */ |
| @Produces({ MediaType.APPLICATION_JSON }) |
| @Consumes({ MediaType.APPLICATION_JSON }) |
| @Path("/{version : " + SERVICE_VERSION_FORMAT + "}/{context}/metadata-catalog/") |
| public class MetadataResource extends AbstractResource { |
| private static final String CLASS_NAME = MetadataResource.class.getName(); |
| private static final Map<Class<?>, String> PRIMITIVE_TO_JSON = new HashMap<Class<?>, String>(); |
| |
| static { |
| PRIMITIVE_TO_JSON.put(boolean.class, "boolean"); |
| PRIMITIVE_TO_JSON.put(byte.class, "number"); |
| PRIMITIVE_TO_JSON.put(char.class, "string"); |
| PRIMITIVE_TO_JSON.put(double.class, "number"); |
| PRIMITIVE_TO_JSON.put(float.class, "number"); |
| PRIMITIVE_TO_JSON.put(int.class, "integer"); |
| PRIMITIVE_TO_JSON.put(long.class, "integer"); |
| PRIMITIVE_TO_JSON.put(short.class, "number"); |
| PRIMITIVE_TO_JSON.put(short.class, "number"); |
| } |
| |
| /** |
| * Returns metadata catalog. |
| */ |
| @GET |
| public Response getMetadataCatalog(@PathParam("version") String version, |
| @PathParam("context") String persistenceUnit, |
| @Context HttpHeaders httpHeaders, |
| @Context UriInfo uriInfo) { |
| setRequestUniqueId(); |
| return buildMetadataCatalogResponse(version, persistenceUnit, httpHeaders, uriInfo); |
| } |
| |
| /** |
| * Returns entity metadata if accepted media type is 'application/json' or entity schema if |
| * accepted media type is 'application/schema+json'. |
| */ |
| @GET |
| @Produces({ MediaType.APPLICATION_JSON, AbstractResource.APPLICATION_SCHEMA_JSON }) |
| @Consumes({ MediaType.APPLICATION_JSON, AbstractResource.APPLICATION_SCHEMA_JSON }) |
| @Path("entity/{entityName}") |
| public Response getEntityResource(@PathParam("version") String version, |
| @PathParam("context") String persistenceUnit, |
| @PathParam("entityName") String entityName, |
| @Context HttpHeaders httpHeaders, |
| @Context UriInfo uriInfo) { |
| setRequestUniqueId(); |
| final MediaType mediaType = StreamingOutputMarshaller.mediaType(httpHeaders.getAcceptableMediaTypes()); |
| |
| // Return schema if application.schema+json media type is requested otherwise return entity metadata |
| if (mediaType.equals(AbstractResource.APPLICATION_SCHEMA_JSON_TYPE)) { |
| return buildEntitySchemaResponse(version, persistenceUnit, entityName, uriInfo); |
| } else { |
| return buildEntityMetadataResponse(version, persistenceUnit, entityName, httpHeaders, uriInfo); |
| } |
| } |
| |
| /** |
| * Returns query metadata if accepted media type is 'application/json' or entity schema if |
| * accepted media type is 'application/schema+json'. |
| */ |
| @GET |
| @Produces({ MediaType.APPLICATION_JSON, AbstractResource.APPLICATION_SCHEMA_JSON }) |
| @Consumes({ MediaType.APPLICATION_JSON, AbstractResource.APPLICATION_SCHEMA_JSON }) |
| @Path("query/{queryName}") |
| public Response getQueryResource(@PathParam("version") String version, |
| @PathParam("context") String persistenceUnit, |
| @PathParam("queryName") String queryName, |
| @Context HttpHeaders httpHeaders, |
| @Context UriInfo uriInfo) { |
| setRequestUniqueId(); |
| final MediaType mediaType = StreamingOutputMarshaller.mediaType(httpHeaders.getAcceptableMediaTypes()); |
| |
| // Return schema if application.schema+json media type is requested otherwise return entity metadata |
| if (mediaType.equals(AbstractResource.APPLICATION_SCHEMA_JSON_TYPE)) { |
| return buildQuerySchemaResponse(version, persistenceUnit, queryName, uriInfo); |
| } else { |
| return buildQueryMetadataResponse(version, persistenceUnit, queryName, httpHeaders, uriInfo); |
| } |
| } |
| |
| private Response buildMetadataCatalogResponse(String version, String persistenceUnit, HttpHeaders httpHeaders, UriInfo uriInfo) { |
| JPARSLogger.entering(CLASS_NAME, "buildMetadataCatalogResponse", new Object[]{"GET", version, persistenceUnit, uriInfo.getRequestUri().toASCIIString()}); |
| |
| final String result; |
| try { |
| final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null); |
| final MetadataCatalog catalog = buildMetadataCatalog(context); |
| final String mediaType = StreamingOutputMarshaller.mediaType(httpHeaders.getAcceptableMediaTypes()).toString(); |
| result = marshallMetadata(catalog, mediaType); |
| } catch (JAXBException e) { |
| throw JPARSException.exceptionOccurred(e); |
| } |
| return Response.ok(new StreamingOutputMarshaller(null, result, httpHeaders.getAcceptableMediaTypes())).build(); |
| } |
| |
| private Response buildEntityMetadataResponse(String version, String persistenceUnit, String entityName, HttpHeaders httpHeaders, UriInfo uriInfo) { |
| JPARSLogger.entering(CLASS_NAME, "buildEntityMetadataResponse", new Object[]{"GET", version, persistenceUnit, entityName, uriInfo.getRequestUri().toASCIIString()}); |
| |
| final String result; |
| try { |
| final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null); |
| final ClassDescriptor descriptor = context.getServerSession().getDescriptorForAlias(entityName); |
| if (descriptor == null) { |
| JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_entity_type", new Object[] { entityName, persistenceUnit }); |
| throw JPARSException.classOrClassDescriptorCouldNotBeFoundForEntity(entityName, persistenceUnit); |
| } else { |
| final String mediaType = StreamingOutputMarshaller.mediaType(httpHeaders.getAcceptableMediaTypes()).toString(); |
| final Resource resource = buildEntityMetadata(context, descriptor); |
| result = marshallMetadata(resource, mediaType); |
| } |
| } catch (JAXBException e) { |
| throw JPARSException.exceptionOccurred(e); |
| } |
| return Response.ok(new StreamingOutputMarshaller(null, result, httpHeaders.getAcceptableMediaTypes())).build(); |
| } |
| |
| private Response buildQueryMetadataResponse(String version, String persistenceUnit, String queryName, HttpHeaders httpHeaders, UriInfo uriInfo) { |
| JPARSLogger.entering(CLASS_NAME, "buildQueryMetadataResponse", new Object[]{"GET", version, persistenceUnit, queryName, uriInfo.getRequestUri().toASCIIString()}); |
| |
| final String result; |
| try { |
| final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null); |
| |
| // We need to make sure that query with given name exists |
| final DatabaseQuery query = context.getServerSession().getQuery(queryName); |
| if (query == null) { |
| JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_query", new Object[] {queryName, persistenceUnit}); |
| throw JPARSException.responseCouldNotBeBuiltForNamedQueryRequest(queryName, context.getName()); |
| } |
| |
| final String mediaType = StreamingOutputMarshaller.mediaType(httpHeaders.getAcceptableMediaTypes()).toString(); |
| final Resource resource = buildQueryMetadata(context, query); |
| result = marshallMetadata(resource, mediaType); |
| } catch (JAXBException e) { |
| throw JPARSException.exceptionOccurred(e); |
| } |
| return Response.ok(new StreamingOutputMarshaller(null, result, httpHeaders.getAcceptableMediaTypes())).build(); |
| } |
| |
| private Response buildEntitySchemaResponse(String version, String persistenceUnit, String entityName, UriInfo uriInfo) { |
| JPARSLogger.entering(CLASS_NAME, "buildEntitySchemaResponse", new Object[]{"GET", version, persistenceUnit, uriInfo.getRequestUri().toASCIIString()}); |
| |
| final String result; |
| try { |
| final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null); |
| final ClassDescriptor descriptor = context.getServerSession().getDescriptorForAlias(entityName); |
| if (descriptor == null) { |
| JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_entity_type", new Object[] { entityName, persistenceUnit }); |
| throw JPARSException.classOrClassDescriptorCouldNotBeFoundForEntity(entityName, persistenceUnit); |
| } else { |
| final ResourceSchema schema = new ResourceSchema(); |
| schema.setTitle(descriptor.getAlias()); |
| schema.setSchema(HrefHelper.buildEntityMetadataHref(context, descriptor.getAlias()) + "#"); |
| schema.addAllOf(new Reference(HrefHelper.buildBaseRestSchemaRef("#/singularResource"))); |
| |
| // Properties |
| for (DatabaseMapping databaseMapping : descriptor.getMappings()) { |
| schema.addProperty(databaseMapping.getAttributeName(), buildProperty(context, databaseMapping)); |
| } |
| |
| // Links |
| final String instancesHref = HrefHelper.buildEntityDescribesHref(context, descriptor.getAlias()); |
| schema.setLinks((new ItemLinksBuilder()) |
| .addDescribedBy(HrefHelper.buildEntityMetadataHref(context, descriptor.getAlias())) |
| .addFind(instancesHref + "/{primaryKey}") |
| .addCreate(instancesHref) |
| .addUpdate(instancesHref) |
| .addDelete(instancesHref + "/{primaryKey}") |
| .getList()); |
| |
| result = marshallMetadata(schema, MediaType.APPLICATION_JSON); |
| } |
| } catch (JAXBException e) { |
| throw JPARSException.exceptionOccurred(e); |
| } |
| return Response.ok(new StreamingOutputMarshaller(null, result, AbstractResource.APPLICATION_SCHEMA_JSON_TYPE)).build(); |
| } |
| |
| private Response buildQuerySchemaResponse(String version, String persistenceUnit, String queryName, UriInfo uriInfo) { |
| JPARSLogger.entering(CLASS_NAME, "buildQuerySchemaResponse", new Object[]{"GET", version, persistenceUnit, uriInfo.getRequestUri().toASCIIString()}); |
| |
| final String result; |
| try { |
| final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null); |
| |
| // We need to make sure that query with given name exists |
| final DatabaseQuery query = context.getServerSession().getQuery(queryName); |
| if (query == null) { |
| JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_query", new Object[] {queryName, persistenceUnit}); |
| throw JPARSException.responseCouldNotBeBuiltForNamedQueryRequest(queryName, context.getName()); |
| } |
| |
| final ResourceSchema querySchema = buildQuerySchema(context, query); |
| result = marshallMetadata(querySchema, MediaType.APPLICATION_JSON); |
| } catch (JAXBException e) { |
| throw JPARSException.exceptionOccurred(e); |
| } |
| return Response.ok(new StreamingOutputMarshaller(null, result, AbstractResource.APPLICATION_SCHEMA_JSON_TYPE)).build(); |
| } |
| |
| private MetadataCatalog buildMetadataCatalog(PersistenceContext context) { |
| final MetadataCatalog result = new MetadataCatalog(); |
| |
| // Entities |
| final Map<Class, ClassDescriptor> descriptors = context.getServerSession().getDescriptors(); |
| for (ClassDescriptor descriptor : descriptors.values()) { |
| |
| // Skip embeddables |
| if (!descriptor.isAggregateDescriptor()) { |
| result.addResource(buildEntityMetadata(context, descriptor)); |
| } |
| } |
| |
| // Queries |
| final Map<String, List<DatabaseQuery>> allQueries = context.getServerSession().getQueries(); |
| for (List<DatabaseQuery> databaseQueries : allQueries.values()) { |
| if (databaseQueries != null) { |
| for (DatabaseQuery query : databaseQueries) { |
| if (query.getReferenceClassName() != null) { |
| result.addResource(buildQueryMetadata(context, query)); |
| } |
| } |
| } |
| } |
| |
| final String href = HrefHelper.buildMetadataCatalogHref(context); |
| final List<LinkV2> links = (new ItemLinksBuilder()) |
| .addCanonical(href) |
| .getList(); |
| |
| result.setLinks(links); |
| return result; |
| } |
| |
| private Resource buildEntityMetadata(PersistenceContext context, ClassDescriptor descriptor) { |
| final Resource resource = new Resource(); |
| resource.setName(descriptor.getAlias()); |
| |
| final String metadataHref = HrefHelper.buildEntityMetadataHref(context, descriptor.getAlias()); |
| final List<LinkV2> links = (new ItemLinksBuilder()) |
| .addAlternate(metadataHref) |
| .addCanonical(metadataHref, MediaType.APPLICATION_JSON) |
| .addDescribes(HrefHelper.buildEntityDescribesHref(context, descriptor.getAlias())) |
| .getList(); |
| |
| resource.setLinks(links); |
| return resource; |
| } |
| |
| private Resource buildQueryMetadata(PersistenceContext context, DatabaseQuery query) { |
| final Resource resource = new Resource(); |
| resource.setName(query.getName()); |
| |
| final String metadataHref = HrefHelper.buildQueryMetadataHref(context, query.getName()); |
| final List<LinkV2> links = (new ItemLinksBuilder()) |
| .addAlternate(metadataHref) |
| .addCanonical(metadataHref, MediaType.APPLICATION_JSON) |
| .addDescribes(HrefHelper.buildQueryDescribesHref(context, query.getName())) |
| .getList(); |
| |
| resource.setLinks(links); |
| return resource; |
| } |
| |
| private ResourceSchema buildQuerySchema(PersistenceContext context, DatabaseQuery query) { |
| final ResourceSchema schema = new ResourceSchema(); |
| schema.setTitle(query.getName()); |
| schema.setSchema(HrefHelper.buildQueryMetadataHref(context, query.getName()) + "#"); |
| schema.addAllOf(new Reference(HrefHelper.buildBaseRestSchemaRef("#/collectionBaseResource"))); |
| |
| // Link |
| final String method = query.isReadQuery() ? "GET" : "POST"; |
| schema.setLinks((new ItemLinksBuilder()) |
| .addExecute(HrefHelper.buildQueryHref(context, query.getName(), getQueryParamString(query)), method) |
| .getList()); |
| |
| // Definitions |
| if (query.isReportQuery()) { |
| // In case of report query we need to define a returned type |
| final ResourceSchema returnType = new ResourceSchema(); |
| |
| query.checkPrepare((AbstractSession) context.getServerSession(), new DatabaseRecord()); |
| for (ReportItem item : ((ReportQuery) query).getItems()) { |
| final Property property; |
| if (item.getMapping() != null) { |
| if (item.getAttributeExpression() != null && item.getAttributeExpression().isMapEntryExpression()) { |
| if (((MapEntryExpression)item.getAttributeExpression()).shouldReturnMapEntry()) { |
| property = buildProperty(context, Map.Entry.class); |
| } else { |
| property = buildProperty(context, ((Class<?>) item.getMapping().getContainerPolicy().getKeyType())); |
| } |
| } else { |
| property = buildProperty(context, item.getMapping().getAttributeClassification()); |
| } |
| } else if (item.getResultType() != null) { |
| property = buildProperty(context, item.getResultType()); |
| } else if (item.getDescriptor() != null) { |
| property = buildProperty(context, item.getDescriptor().getJavaClass()); |
| } else if (item.getAttributeExpression() != null && item.getAttributeExpression().isConstantExpression()) { |
| property = buildProperty(context, ((ConstantExpression) item.getAttributeExpression()).getValue().getClass()); |
| } else { |
| // Use Object.class by default. |
| property = buildProperty(context, Object.class); |
| } |
| returnType.addProperty(item.getName(), property); |
| } |
| schema.addDefinition("result", returnType); |
| |
| final Property items = new Property(); |
| items.setType("array"); |
| items.setItems(new Property("#/definitions/result")); |
| schema.addProperty("items", items); |
| } else { |
| // Read all query. Each item is an entity. Make a JSON pointer. |
| if (query.getReferenceClassName() != null) { |
| final Property items = new Property(); |
| items.setType("array"); |
| items.setItems(new Property(HrefHelper.buildEntityMetadataHref(context, query.getReferenceClass().getSimpleName()) + "#")); |
| schema.addProperty("items", items); |
| } |
| } |
| |
| return schema; |
| } |
| |
| private String getQueryParamString(DatabaseQuery query) { |
| final StringBuilder queryParams = new StringBuilder(); |
| for (String arg : query.getArguments()) { |
| queryParams.append(";"); |
| queryParams.append(arg).append("={").append(arg).append("}"); |
| } |
| return queryParams.toString(); |
| } |
| |
| private Property buildProperty(PersistenceContext context, DatabaseMapping mapping) { |
| if (mapping.isCollectionMapping()) { |
| final Property property = new Property(); |
| property.setType("array"); |
| property.setItems(buildProperty(context, getCollectionGenericClass(mapping))); |
| return property; |
| } else if (mapping.isForeignReferenceMapping()) { |
| final ForeignReferenceMapping foreignReferenceMapping = (ForeignReferenceMapping)mapping; |
| final String href = HrefHelper.buildEntityMetadataHref(context, foreignReferenceMapping.getReferenceClass().getSimpleName() + "#"); |
| return new Property(href); |
| } else { |
| return buildProperty(context, mapping.getAttributeClassification()); |
| } |
| } |
| |
| private Property buildProperty(PersistenceContext context, Class<?> clazz) { |
| final Property property = new Property(); |
| if (context.getServerSession().getDescriptorForAlias(clazz.getSimpleName()) != null) { |
| property.setRef(HrefHelper.buildEntityMetadataHref(context, clazz.getSimpleName()) + "#"); |
| } else if (Number.class.isAssignableFrom(clazz)) { |
| property.setType("number"); |
| } else if (Boolean.class.equals(clazz)) { |
| property.setType("boolean"); |
| } else if (String.class.equals(clazz)) { |
| property.setType("string"); |
| } else if (Collection.class.isAssignableFrom(clazz)) { |
| property.setType("array"); |
| } else if (clazz.isPrimitive()) { |
| property.setType(PRIMITIVE_TO_JSON.get(clazz)); |
| } else { |
| property.setType("object"); |
| } |
| |
| return property; |
| } |
| |
| private Class<?> getCollectionGenericClass(DatabaseMapping mapping) { |
| Class<?> collectionName = null; |
| if (mapping.isEISMapping()) { |
| final EISCompositeCollectionMapping collectionMapping = (EISCompositeCollectionMapping) mapping; |
| if (collectionMapping.getReferenceClass() != null) { |
| collectionName = collectionMapping.getReferenceClass(); |
| } |
| if ((collectionName == null) && (collectionMapping.getAttributeClassification() != null)) { |
| collectionName = collectionMapping.getAttributeClassification(); |
| } |
| } else { |
| final CollectionMapping collectionMapping = (CollectionMapping) mapping; |
| if (collectionMapping.getReferenceClass() != null) { |
| collectionName = collectionMapping.getReferenceClass(); |
| } |
| if ((collectionName == null) && (collectionMapping.getAttributeClassification() != null)) { |
| collectionName = collectionMapping.getAttributeClassification(); |
| } |
| } |
| |
| return collectionName; |
| } |
| } |