/**
* 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.io.StringWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.http.HttpStatus;
import org.core4j.Enumerable;
import org.odata4j.core.ODataConstants;
import org.odata4j.core.ODataVersion;
import org.odata4j.core.OEntityId;
import org.odata4j.core.OEntityIds;
import org.odata4j.core.OEntityKey;
import org.odata4j.edm.EdmDataServices;
import org.odata4j.edm.EdmMultiplicity;
import org.odata4j.format.FormatParser;
import org.odata4j.format.FormatParserFactory;
import org.odata4j.format.FormatType;
import org.odata4j.format.FormatWriter;
import org.odata4j.format.Settings;
import org.odata4j.format.SingleLink;
import org.odata4j.format.SingleLinks;
import org.odata4j.producer.EntityIdResponse;
import org.odata4j.producer.ODataProducer;
import org.odata4j.producer.QueryInfo;
import org.odata4j.producer.exceptions.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fujitsu.dc.common.utils.DcCoreUtils;
import com.fujitsu.dc.core.DcCoreException;
import com.fujitsu.dc.core.auth.AccessContext;
import com.fujitsu.dc.core.model.ctl.Account;
import com.fujitsu.dc.core.model.ctl.Common;
import com.fujitsu.dc.core.model.ctl.ReceivedMessage;
import com.fujitsu.dc.core.model.impl.es.odata.EsODataProducer;
import com.fujitsu.dc.core.odata.DcFormatWriterFactory;
/**
* ODataの$linksを扱う JAX-RS Resource.
*/
public final class ODataLinksResource {
private final OEntityId sourceEntity;
private final String targetNavProp;
private final OEntityKey targetEntityKey;
private final ODataResource odataResource;
private final ODataProducer odataProducer;
private final AccessContext accessContext;
/**
* ログ.
*/
static Logger log = LoggerFactory.getLogger(ODataLinksResource.class);
/**
* コンストラクタ.
* @param odataResource 親の ODataResource
* @param sourceEntity リンク元Entity
* @param targetNavProp リンク先 Navigation Property
* @param targetEntityKey リンク先 EntityKey
*/
public ODataLinksResource(
final ODataResource odataResource,
final OEntityId sourceEntity,
final String targetNavProp,
final OEntityKey targetEntityKey) {
this.odataResource = odataResource;
this.accessContext = this.odataResource.getAccessContext();
this.odataProducer = this.odataResource.getODataProducer();
this.sourceEntity = sourceEntity;
this.targetNavProp = targetNavProp;
this.targetEntityKey = targetEntityKey;
}
/**
* POSTメソッドを受けて linkを作成する.
* 成功時のレスポンスは204.特に記述が無いためLocationヘッダは返さない。
* InsertLink Request
* If an InsertLink Request is successful, the response MUST have a 204 status code,
* as specified in [RFC2616], and contain an empty response body.
* @param uriInfo UriInfo
* @param reqBody リクエストボディ
* @return JAX-RS Response
*/
@POST
public Response createLink(
@Context UriInfo uriInfo,
final Reader reqBody) {
// アクセス制御
this.checkWriteAccessContext();
// リンク作成前処理
this.odataResource.beforeLinkCreate(this.sourceEntity, this.targetNavProp);
// $links の POSTでNav Propのキー指定があってはいけない。
if (this.targetEntityKey != null) {
throw DcCoreException.OData.KEY_FOR_NAVPROP_SHOULD_NOT_BE_SPECIFIED;
}
log.debug("POSTING $LINK");
OEntityId newTargetEntity =
parseRequestUri(DcCoreUtils.createUriInfo(uriInfo, NUM_LEVELS_FROM_SVC_ROOT), reqBody);
// URLで指定したリンク先オブジェクトとBodyに指定したオブジェクトが等しいかをチェックする
Pattern p = Pattern.compile("(.+)/([^/]+)$");
Matcher m = p.matcher(newTargetEntity.getEntitySetName());
String bodyNavProp = m.replaceAll("$2");
String targetEntitySetName = null;
// 受信メッセージとアカウントの$linksの場合、リンク先にAccountを設定
if (ReceivedMessage.EDM_NPNAME_FOR_ACCOUNT.equals(this.targetNavProp)) {
targetEntitySetName = Account.EDM_TYPE_NAME;
// アカウントと受信メッセージの$linksの場合、リンク先にReceivedMessageを設定
} else if (Account.EDM_NPNAME_FOR_RECEIVED_MESSAGE.equals(this.targetNavProp)) {
targetEntitySetName = ReceivedMessage.EDM_TYPE_NAME;
} else {
targetEntitySetName = this.targetNavProp.substring(1);
}
if (!targetEntitySetName.equals(bodyNavProp)) {
throw DcCoreException.OData.REQUEST_FIELD_FORMAT_ERROR.params(Common.DC_FORMAT_PATTERN_URI);
}
this.odataProducer.createLink(sourceEntity, targetNavProp, newTargetEntity);
return noContent();
}
/**
* PUTメソッドを受けて linkを更新する.
* @param uriInfo UriInfo
* @param reqBody リクエストボディ
* @return JAX-RS Response
*/
@PUT
public Response updateLink(
@Context UriInfo uriInfo,
final Reader reqBody) {
// アクセス制御
this.checkWriteAccessContext();
if (this.targetEntityKey == null) {
throw DcCoreException.OData.KEY_FOR_NAVPROP_SHOULD_BE_SPECIFIED;
} else {
throw DcCoreException.Misc.METHOD_NOT_IMPLEMENTED;
}
}
/**
* リクエストボディで指定された値が正しい形式かチェックし、$links先のOEntityIdを返却する.
* @param uriInfo リクエストURL
* @param reqBody リクエストボディ
* @param srcEntitySetName $links元EntitySet名
* @param metadata メタデータ
* @return $links先のOEntityId
*/
static OEntityId parseRequestUri(final UriInfo uriInfo,
final Reader reqBody,
String srcEntitySetName,
EdmDataServices metadata) {
Settings settings = new Settings(ODataVersion.V1, metadata, srcEntitySetName, null, null);
FormatParser<SingleLink> parser = FormatParserFactory.getParser(SingleLink.class, FormatType.JSON, settings);
SingleLink link = null;
try {
link = parser.parse(reqBody);
} catch (Exception e) {
throw DcCoreException.OData.JSON_PARSE_ERROR.reason(e);
}
if (link.getUri() == null) {
throw DcCoreException.OData.REQUEST_FIELD_FORMAT_ERROR.params("uri");
}
log.debug(uriInfo.getBaseUri().toASCIIString());
// 複合キー対応
// nullが指定されているとパースに失敗するため、null値が設定されている場合はダミーキーに置き換える
String linkUrl = AbstractODataResource.replaceNullToDummyKey(link.getUri());
log.debug(linkUrl);
OEntityId oid = null;
String serviceRootUri = uriInfo.getBaseUri().toASCIIString();
try {
oid = OEntityIds.parse(serviceRootUri, linkUrl);
} catch (IllegalArgumentException e) {
throw DcCoreException.OData.REQUEST_FIELD_FORMAT_ERROR.params("uri");
}
// parse処理では後ろ括弧のチェックの対応を行っていないため、括弧の対応チェックを行う
String entityId = linkUrl;
if (entityId.toLowerCase().startsWith(serviceRootUri.toLowerCase())) {
entityId = linkUrl.substring(serviceRootUri.length());
}
int indexOfParen = entityId.indexOf('(');
String entitySetName = entityId.substring(indexOfParen);
Pattern p = Pattern.compile("^\\(.+\\)$");
Matcher m = p.matcher(entitySetName);
if (!m.find()) {
throw DcCoreException.OData.REQUEST_FIELD_FORMAT_ERROR.params("uri");
}
return oid;
}
/**
* リクエストボディで指定された値が正しい形式かチェックし、$links先のOEntityIdを返却する.
* @param uriInfo リクエストURL
* @param reqBody リクエストボディ
* @return $links先のOEntityId
*/
private OEntityId parseRequestUri(final UriInfo uriInfo, final Reader reqBody) {
return parseRequestUri(uriInfo, reqBody, this.sourceEntity.getEntitySetName(),
this.odataProducer.getMetadata());
}
private Response noContent() {
return Response.noContent().header(ODataConstants.Headers.DATA_SERVICE_VERSION, ODataVersion.V2.asString)
.build();
}
/**
* DELETEメソッドを受けて linkを削除する.
* @return JAX-RS Response
*/
@DELETE
public Response deleteLink() {
// アクセス制御
this.checkWriteAccessContext();
// リンク削除前処理
this.odataResource.beforeLinkDelete(this.sourceEntity, this.targetNavProp);
// TODO $links の 削除は以下の2つのリクエストで実行可能であるが、NavPropのKey未指定は未実装
// 1. http://host/service.svc/Customers('ALFKI')/$links/Orders(1)
// 2. http://host/service.svc/Orders(1)/$links/Customer.
if (this.targetEntityKey == null) {
throw DcCoreException.OData.KEY_FOR_NAVPROP_SHOULD_BE_SPECIFIED;
}
this.odataProducer.deleteLink(sourceEntity, targetNavProp, targetEntityKey);
return noContent();
}
static final int NUM_LEVELS_FROM_SVC_ROOT = 3;
/**
* GETメソッドを受けて link一覧を返す.
* @param uriInfo UriInfo
* @param format $format
* @param callback ??
* @return JAX-RS Response
*/
@GET
public Response getLinks(
@Context final UriInfo uriInfo,
@QueryParam("$format") final String format,
@QueryParam("$callback") final String callback) {
// アクセス制御
this.checkReadAccessContext();
if (this.targetEntityKey != null) {
return Response
.status(HttpStatus.SC_BAD_REQUEST)
.entity("targetId should not be specified in $links GET. your value = "
+ this.targetEntityKey.toKeyString()).build();
}
log.debug("GETTING $LINK");
// リンク取得前処理
this.odataResource.beforeLinkGet(this.sourceEntity, this.targetNavProp);
EntityIdResponse response = getLinks(uriInfo);
StringWriter sw = new StringWriter();
// context.getRequest().getAcceptableMediaTypes()
UriInfo uriInfo2 = DcCoreUtils.createUriInfo(uriInfo, NUM_LEVELS_FROM_SVC_ROOT);
String serviceRootUri = uriInfo2.getBaseUri().toASCIIString();
String contentType;
if (response.getMultiplicity() == EdmMultiplicity.MANY) {
SingleLinks links = SingleLinks.create(serviceRootUri, response.getEntities());
// TODO レスポンスはJSON固定とする.
FormatWriter<SingleLinks> fw = DcFormatWriterFactory.getFormatWriter(SingleLinks.class, null, "json",
callback);
fw.write(uriInfo2, sw, links);
contentType = fw.getContentType();
} else {
OEntityId entityId = Enumerable.create(response.getEntities()).firstOrNull();
if (entityId == null) {
throw new NotFoundException();
}
SingleLink link = SingleLinks.create(serviceRootUri, entityId);
FormatWriter<SingleLink> fw = DcFormatWriterFactory.getFormatWriter(SingleLink.class, null, "json",
callback);
fw.write(uriInfo, sw, link);
contentType = fw.getContentType();
}
String entity = sw.toString();
return Response.ok(entity, contentType)
.header(ODataConstants.Headers.DATA_SERVICE_VERSION, ODataVersion.V2.asString)
.build();
}
/**
* OPTIONSメソッド.
* @return JAX-RS Response
*/
@OPTIONS
public Response options() {
// アクセス制御
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryOptionsPrivilege());
return DcCoreUtils.responseBuilderForOptions(
HttpMethod.GET,
HttpMethod.DELETE,
HttpMethod.PUT,
HttpMethod.POST
).build();
}
private EntityIdResponse getLinks(UriInfo uriInfo) {
QueryInfo queryInfo = null;
if (uriInfo != null) {
queryInfo = queryInfo(uriInfo);
}
EntityIdResponse response = null;
// getLinksメソッドにパラメータを追加したが、インターフェースを修正すると影響範囲が大きいため、
// プロデューサーをキャストする
if (this.odataProducer instanceof EsODataProducer) {
EsODataProducer producer = (EsODataProducer) this.odataProducer;
response = producer.getLinks(sourceEntity, targetNavProp, queryInfo);
}
return response;
}
QueryInfo queryInfo(UriInfo uriInfo) {
MultivaluedMap<String, String> mm = uriInfo.getQueryParameters(true);
Integer top = QueryParser.parseTopQuery(mm.getFirst("$top"));
Integer skip = QueryParser.parseSkipQuery(mm.getFirst("$skip"));
return new QueryInfo(
null,
top,
skip,
null,
null,
null,
null,
null,
null);
}
private void checkWriteAccessContext() {
// アクセス制御
// TODO BOXレベルの場合に同じ処理が2回走る。無駄なのでcheckAccessContextにPrivilegeを配列で渡す等の工夫が必要
String entitySetNameFrom = sourceEntity.getEntitySetName();
String entitySetNameTo = targetNavProp;
if (entitySetNameFrom.equals(ReceivedMessage.EDM_TYPE_NAME)
|| entitySetNameTo.equals(Account.EDM_NPNAME_FOR_RECEIVED_MESSAGE)) {
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryWritePrivilege(ReceivedMessage.EDM_TYPE_NAME));
} else {
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryWritePrivilege(entitySetNameFrom));
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryWritePrivilege(entitySetNameTo.substring(1)));
}
}
private void checkReadAccessContext() {
// アクセス制御
// TODO BOXレベルの場合に同じ処理が2回走る。無駄なのでcheckAccessContextにPrivilegeを配列で渡す等の工夫が必要
String entitySetNameFrom = sourceEntity.getEntitySetName();
String entitySetNameTo = targetNavProp;
if (entitySetNameFrom.equals(ReceivedMessage.EDM_TYPE_NAME)
|| entitySetNameTo.equals(Account.EDM_NPNAME_FOR_RECEIVED_MESSAGE)) {
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryReadPrivilege(ReceivedMessage.EDM_TYPE_NAME));
} else {
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryReadPrivilege(entitySetNameFrom));
this.odataResource.checkAccessContext(this.accessContext,
this.odataResource.getNecessaryReadPrivilege(entitySetNameTo.substring(1)));
}
}
}