/** * personium.io * Copyright 2014 FUJITSU LIMITED * * Licensed 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 com.fujitsu.dc.core.rs.odata; import java.io.Reader; import java.util.List; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.HttpMethod; import javax.ws.rs.OPTIONS; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.UriInfo; import org.odata4j.core.ODataConstants; import org.odata4j.core.ODataVersion; import org.odata4j.core.OEntity; import org.odata4j.core.OEntityId; import org.odata4j.core.OEntityIds; import org.odata4j.core.OEntityKey; import org.odata4j.edm.EdmDataServices; import org.odata4j.edm.EdmEntitySet; import org.odata4j.edm.EdmEntityType; import org.odata4j.expression.EntitySimpleProperty; import org.odata4j.producer.EntityQueryInfo; import org.odata4j.producer.EntityResponse; import org.odata4j.producer.resources.OptionsQueryParser; import com.fujitsu.dc.common.utils.DcCoreUtils; import com.fujitsu.dc.core.DcCoreConfig; import com.fujitsu.dc.core.DcCoreException; import com.fujitsu.dc.core.annotations.MERGE; import com.fujitsu.dc.core.auth.AccessContext; import com.fujitsu.dc.core.model.ctl.ReceivedMessage; import com.fujitsu.dc.core.model.ctl.SentMessage; import com.fujitsu.dc.core.odata.DcODataProducer; import com.fujitsu.dc.core.odata.DcOptionsQueryParser; import com.fujitsu.dc.core.odata.OEntityWrapper; /** * ODataのEntityリソース(id指定されたURL)を扱うJAX-RS リソース. */ public class ODataEntityResource extends AbstractODataResource { // public static final String PATH_CELL_ID = "cellid"; private final String keyString; private final ODataResource odataResource; private final AccessContext accessContext; /** * @return AccessContext */ public AccessContext getAccessContext() { return accessContext; } /** * @return the odataResource */ public ODataResource getOdataResource() { return odataResource; } private OEntityKey oEntityKey; /** * @return the odataResource */ public OEntityKey getOEntityKey() { return this.oEntityKey; } /** * このリソースが担当する ODataリソースの OEntityIdオブジェクト. * @return OEntityIdオブジェクト */ public OEntityId getOEntityId() { return OEntityIds.create(getEntitySetName(), this.oEntityKey); } /** * コンストラクタ. */ public ODataEntityResource() { this.odataResource = null; this.accessContext = null; this.keyString = null; this.oEntityKey = null; } /** * コンストラクタ. * @param odataResource 親リソースであるODataResource * @param entitySetName EntitySet Name * @param key キー文字列 */ public ODataEntityResource(final ODataResource odataResource, final String entitySetName, final String key) { this.odataResource = odataResource; this.accessContext = this.odataResource.accessContext; setOdataProducer(this.odataResource.getODataProducer()); setEntitySetName(entitySetName); // 複合キー対応 // nullが指定されているとパースに失敗するため、null値が設定されている場合はダミーキーに置き換える this.keyString = AbstractODataResource.replaceNullToDummyKeyWithParenthesis(key); try { this.oEntityKey = OEntityKey.parse(this.keyString); } catch (IllegalArgumentException e) { throw DcCoreException.OData.ENTITY_KEY_PARSE_ERROR.reason(e); } EdmDataServices metadata = getOdataProducer().getMetadata(); EdmEntitySet edmEntitySet = metadata.findEdmEntitySet(entitySetName); if (edmEntitySet == null) { throw DcCoreException.OData.NO_SUCH_ENTITY_SET; } EdmEntityType edmEntityType = edmEntitySet.getType(); validatePrimaryKey(oEntityKey, edmEntityType); } /** * GETメソッドの処理. * @param uriInfo UriInfo * @param accept Accept ヘッダ * @param ifNoneMatch If-None-Match ヘッダ * @param format $format パラメタ * @param expand $expand パラメタ * @param select $select パラメタ * @return JAX-RSResponse */ @GET public Response get( @Context final UriInfo uriInfo, @HeaderParam(HttpHeaders.ACCEPT) String accept, @HeaderParam(HttpHeaders.IF_NONE_MATCH) String ifNoneMatch, @QueryParam("$format") String format, @QueryParam("$expand") String expand, @QueryParam("$select") String select) { // アクセス制御 this.odataResource.checkAccessContext(this.accessContext, this.odataResource.getNecessaryReadPrivilege(getEntitySetName())); UriInfo resUriInfo = DcCoreUtils.createUriInfo(uriInfo, 1); // $formatとAcceptヘッダの値から出力形式を決定 MediaType contentType = decideOutputFormat(accept, format); String outputFormat = FORMAT_JSON; if (MediaType.APPLICATION_ATOM_XML_TYPE.equals(contentType)) { outputFormat = FORMAT_ATOM; } // Entityの取得をProducerに依頼 EntityResponse entityResp = getEntity(expand, select, resUriInfo); String respStr = renderEntityResponse(resUriInfo, entityResp, outputFormat, null); // 制御コードのエスケープ処理 respStr = escapeResponsebody(respStr); ResponseBuilder rb = Response.ok().type(contentType); rb.header(ODataConstants.Headers.DATA_SERVICE_VERSION, ODataVersion.V2.asString); // ETagを正式実装するときに、返却する必要がある OEntity entity = entityResp.getEntity(); String etag = null; // 基本的にこのIF文に入る。 if (entity instanceof OEntityWrapper) { OEntityWrapper oew = (OEntityWrapper) entity; // エンティティごとのアクセス可否判断 this.odataResource.checkAccessContextPerEntity(this.accessContext, oew); etag = oew.getEtag(); // 基本的にこのIF文に入る。 if (etag != null) { // If-None-Matchヘッダの指定があるとき if (ifNoneMatch != null && ifNoneMatch.equals(ODataResource.renderEtagHeader(etag))) { return Response.notModified().build(); } // ETagヘッダの付与 rb.header(HttpHeaders.ETAG, ODataResource.renderEtagHeader(etag)); } } return rb.entity(respStr).build(); } /** * Entityの取得をProducerに依頼. * @param expand expand * @param select select * @param resUriInfo UriInfo * @return EntityResponse */ EntityResponse getEntity(String expand, String select, UriInfo resUriInfo) { EntityQueryInfo queryInfo = null; if (resUriInfo != null) { queryInfo = queryInfo(expand, select, resUriInfo); } EntityResponse entityResp = getOdataProducer().getEntity(getEntitySetName(), this.oEntityKey, queryInfo); return entityResp; } EntityQueryInfo queryInfo(String expand, String select, UriInfo resUriInfo) { List<EntitySimpleProperty> selects = null; List<EntitySimpleProperty> expands = null; // $select if ("".equals(select)) { throw DcCoreException.OData.SELECT_PARSE_ERROR; } if ("*".equals(select)) { select = null; } try { selects = DcOptionsQueryParser.parseSelect(select); } catch (Exception e) { throw DcCoreException.OData.SELECT_PARSE_ERROR.reason(e); } try { expands = DcOptionsQueryParser.parseExpand(expand); } catch (Exception e) { throw DcCoreException.OData.EXPAND_PARSE_ERROR.reason(e); } // $expandに指定されたプロパティ数の上限チェック if (expands != null && expands.size() > DcCoreConfig.getExpandPropertyMaxSizeForRetrieve()) { throw DcCoreException.OData.EXPAND_COUNT_LIMITATION_EXCEEDED; } EntityQueryInfo queryInfo = new EntityQueryInfo(null, OptionsQueryParser.parseCustomOptions(resUriInfo), expands, selects); return queryInfo; } /** * PUT メソッドの処理. * @param reader リクエストボディ * @param accept Accept ヘッダ * @param ifMatch If-Match ヘッダ * @return JAX-RSResponse */ @PUT public Response put(Reader reader, @HeaderParam(HttpHeaders.ACCEPT) final String accept, @HeaderParam(HttpHeaders.IF_MATCH) final String ifMatch) { // メソッド実行可否チェック checkNotAllowedMethod(); // アクセス制御 this.odataResource.checkAccessContext(this.accessContext, this.odataResource.getNecessaryWritePrivilege(getEntitySetName())); String etag; // リクエストの更新をProducerに依頼 OEntityWrapper oew = updateEntity(reader, ifMatch); // 特に例外があがらなければ、レスポンスを返す。 // oewに新たに登録されたETagを返す etag = oew.getEtag(); return Response.noContent() .header(HttpHeaders.ETAG, ODataResource.renderEtagHeader(etag)) .header(ODataConstants.Headers.DATA_SERVICE_VERSION, ODataVersion.V2.asString) .build(); } /** * リクエストの更新をProducerに依頼. * @param reader リクエストボディ * @param ifMatch ifMatch * @return OEntityWrapper */ OEntityWrapper updateEntity(Reader reader, final String ifMatch) { // リクエストからOEntityWrapperを作成する. OEntity oe = this.createRequestEntity(reader, this.oEntityKey); OEntityWrapper oew = new OEntityWrapper(null, oe, null); // 必要ならばメタ情報をつける処理 this.odataResource.beforeUpdate(oew, this.oEntityKey); // If-Matchヘッダで入力されたETagをMVCC用での衝突検知用にOEntityWrapperに設定する。 String etag = ODataResource.parseEtagHeader(ifMatch); oew.setEtag(etag); // UPDATE処理をODataProducerに依頼。 // こちらでリソースの存在確認もしてもらう。 getOdataProducer().updateEntity(getEntitySetName(), this.oEntityKey, oew); return oew; } /** * MERGE メソッドの処理. * @param reader リクエストボディ * @param accept Accept ヘッダ * @param ifMatch If-Match ヘッダ * @return JAX-RSResponse */ @MERGE public Response merge(Reader reader, @HeaderParam(HttpHeaders.ACCEPT) final String accept, @HeaderParam(HttpHeaders.IF_MATCH) final String ifMatch) { ODataMergeResource oDataMergeResource = new ODataMergeResource(this.odataResource, this.getEntitySetName(), this.keyString); return oDataMergeResource.merge(reader, accept, ifMatch); } /** * DELETEメソッドの処理. * @param accept Accept ヘッダ * @param ifMatch If-Match ヘッダ * @return JAX-RS Response */ @DELETE public Response delete( @HeaderParam(HttpHeaders.ACCEPT) final String accept, @HeaderParam(HttpHeaders.IF_MATCH) final String ifMatch) { // アクセス制御 this.odataResource.checkAccessContext(this.accessContext, this.odataResource.getNecessaryWritePrivilege(getEntitySetName())); deleteEntity(ifMatch); return Response.noContent().header(ODataConstants.Headers.DATA_SERVICE_VERSION, ODataVersion.V2.asString) .build(); } /** * Entityの削除をProducerに依頼. * @param ifMatch */ void deleteEntity(final String ifMatch) { // 削除前処理 this.odataResource.beforeDelete(getEntitySetName(), this.oEntityKey); String etag = ODataResource.parseEtagHeader(ifMatch); // 削除処理 DcODataProducer op = this.getOdataProducer(); op.deleteEntity(getEntitySetName(), this.oEntityKey, etag); // 削除後処理 this.odataResource.afterDelete(getEntitySetName(), this.oEntityKey); } /** * $links/{navProp} というパスの処理. * ODataLinksResourceに処理を委譲. * @param targetNavProp Navigation Property * @return ODataLinksResource オブジェクト */ @Path("{first: \\$}links/{targetNavProp:.+?}") public ODataLinksResource links(@PathParam("targetNavProp") final String targetNavProp) { OEntityKey oeKey = OEntityKey.parse(this.keyString); OEntityId oeId = OEntityIds.create(getEntitySetName(), oeKey); return new ODataLinksResource(this.odataResource, oeId, targetNavProp, null); } /** * $links/{navProp}({targetKey})というパスの処理. * ODataLinksResourceに処理を委譲. * @param targetNavProp ターゲット NavigationPropert * @param targetId ターゲットのID * @return ODataLinksResourceオブジェクト */ @Path("{first: \\$}links/{targetNavProp:.+?}({targetId})") public ODataLinksResource link(@PathParam("targetNavProp") final String targetNavProp, @PathParam("targetId") final String targetId) { OEntityKey targetEntityKey = null; try { if (targetId != null && !targetId.isEmpty()) { // 複合キー対応 // nullが指定されているとパースに失敗するため、null値が設定されている場合はダミーキーに置き換える String targetKey = AbstractODataResource.replaceNullToDummyKeyWithParenthesis(targetId); targetEntityKey = OEntityKey.parse(targetKey); } } catch (IllegalArgumentException e) { throw DcCoreException.OData.ENTITY_KEY_LINKS_PARSE_ERROR.reason(e); } OEntityId oeId = OEntityIds.create(getEntitySetName(), this.oEntityKey); return new ODataLinksResource(this.odataResource, oeId, targetNavProp, targetEntityKey); } /** * {navProp:.+}というパスに対する処理を ODataPropertyResourceに飛ばす. * @param navProp Navigation Property * @return ODataPropertyResource Object */ @Path("{navProp: _.+}") public ODataPropertyResource getNavProperty(@PathParam("navProp") final String navProp) { return new ODataPropertyResource(this, navProp); } /** * NavigationProperty経由はID指定は不可のため404とする. * @param navProp Navigation Property * @param targetId ターゲットのID * @return ODataPropertyResource Object */ @Path("{navProp: _.+}({targetId})") public ODataPropertyResource getNavProperty(@PathParam("navProp") final String navProp, @PathParam("targetId") final String targetId) { throw DcCoreException.OData.KEY_FOR_NAVPROP_SHOULD_NOT_BE_SPECIFIED; } /** * OPTIONSメソッド. * @return JAX-RS Response */ @OPTIONS public Response options() { // アクセス制御 this.odataResource.checkAccessContext(this.accessContext, this.odataResource.getNecessaryReadPrivilege(getEntitySetName())); return DcCoreUtils.responseBuilderForOptions( HttpMethod.GET, HttpMethod.PUT, DcCoreUtils.HttpMethod.MERGE, HttpMethod.DELETE ).build(); } /** * メソッド実行可否チェック. */ protected void checkNotAllowedMethod() { if (ReceivedMessage.EDM_TYPE_NAME.equals(getEntitySetName()) || SentMessage.EDM_TYPE_NAME.equals(getEntitySetName())) { throw DcCoreException.Misc.METHOD_NOT_ALLOWED; } } }