/** * 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.model.impl.fs; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.StreamingOutput; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; import org.apache.wink.webdav.model.Multistatus; import org.apache.wink.webdav.model.ObjectFactory; import org.apache.wink.webdav.model.Prop; import org.apache.wink.webdav.model.Propertyupdate; import org.apache.wink.webdav.model.Response; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.odata4j.producer.CountResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.fujitsu.dc.common.auth.token.Role; import com.fujitsu.dc.common.es.util.IndexNameEncoder; import com.fujitsu.dc.common.utils.DcCoreUtils; import com.fujitsu.dc.core.DcCoreConfig; import com.fujitsu.dc.core.DcCoreException; import com.fujitsu.dc.core.DcCoreLog; import com.fujitsu.dc.core.auth.AccessContext; import com.fujitsu.dc.core.auth.BoxPrivilege; import com.fujitsu.dc.core.auth.OAuth2Helper.Key; import com.fujitsu.dc.core.http.header.ByteRangeSpec; import com.fujitsu.dc.core.http.header.RangeHeaderHandler; import com.fujitsu.dc.core.model.Box; import com.fujitsu.dc.core.model.Cell; import com.fujitsu.dc.core.model.DavCmp; import com.fujitsu.dc.core.model.DavDestination; import com.fujitsu.dc.core.model.ModelFactory; import com.fujitsu.dc.core.model.ctl.ComplexType; import com.fujitsu.dc.core.model.ctl.EntityType; import com.fujitsu.dc.core.model.file.BinaryDataAccessor; import com.fujitsu.dc.core.model.file.BinaryDataNotFoundException; import com.fujitsu.dc.core.model.file.StreamingOutputForDavFile; import com.fujitsu.dc.core.model.file.StreamingOutputForDavFileWithRange; import com.fujitsu.dc.core.model.impl.es.odata.UserSchemaODataProducer; import com.fujitsu.dc.core.model.jaxb.Ace; import com.fujitsu.dc.core.model.jaxb.Acl; import com.fujitsu.dc.core.model.jaxb.ObjectIo; import com.fujitsu.dc.core.model.lock.Lock; import com.fujitsu.dc.core.model.lock.LockKeyComposer; import com.fujitsu.dc.core.model.lock.LockManager; import com.fujitsu.dc.core.odata.DcODataProducer; /** * DavCmp implementation using FileSystem. */ public class DavCmpFsImpl implements DavCmp { String fsPath; File fsDir; Box box; Cell cell; ObjectFactory of; String name; Acl acl; DavMetadataFile metaFile; DavCmpFsImpl parent; List<String> ownerRepresentativeAccounts = new ArrayList<String>(); boolean isPhantom = false; /** * Fixed File Name for storing file. */ private static final String CONTENT_FILE_NAME = "content"; private static final String TEMP_FILE_NAME = "tmp"; /* * logger. */ private static Logger log = LoggerFactory.getLogger(DavCmpFsImpl.class); DavCmpFsImpl() { } /** * constructor. * @param name * name of the path component * @param parent * parent DavCmp object * @param cell * Cell * @param box * Box */ private DavCmpFsImpl(final String name, final DavCmpFsImpl parent) { this.name = name; this.of = new ObjectFactory(); this.parent = parent; if (parent == null) { this.metaFile = DavMetadataFile.newInstance(this); return; } this.cell = parent.getCell(); this.box = parent.getBox(); this.fsPath = this.parent.fsPath + File.separator + this.name; this.fsDir = new File(this.fsPath); this.metaFile = DavMetadataFile.newInstance(this); } /** * create a DavCmp whose path most probably does not yet exist. * There still are possibilities that other thread creates the corresponding resource and * the path actually exists. * @param name path name * @param parent parent DavCmp * @return created DavCmp */ public static DavCmpFsImpl createPhantom(final String name, final DavCmpFsImpl parent) { DavCmpFsImpl ret = new DavCmpFsImpl(name, parent); ret.isPhantom = true; return ret; } /** * create a DavCmp whose path most probably does exist. * There still are possibilities that other thread deletes the corresponding resource and * the path actually does not exists. * @param name path name * @param parent parent DavCmp * @return created DavCmp */ public static DavCmpFsImpl create(final String name, final DavCmpFsImpl parent) { DavCmpFsImpl ret = new DavCmpFsImpl(name, parent); if (ret.exists()) { ret.load(); } else { ret.isPhantom = true; } return ret; } void createNewMetadataFile() { this.metaFile = DavMetadataFile.prepareNewFile(this, this.getType()); this.metaFile.save(); } @Override public boolean isEmpty() { if (!this.exists()) { return true; } String type = this.getType(); if (DavCmp.TYPE_COL_WEBDAV.equals(type)) { return !(this.getChildrenCount() > 0); } else if (DavCmp.TYPE_COL_BOX.equals(type)) { return !(this.getChildrenCount() > 0); } else if (DavCmp.TYPE_COL_ODATA.equals(type)) { // Collectionに紐づくEntityTypeの一覧を取得する // EntityTypeに紐づくリソース(AsssociationEndなど)はEntityTypeが必ず親となる関係であるため // EntityTypeのみ検索すれば、EntityTypeに紐づくリソースまでチェックする必要はない UserSchemaODataProducer producer = new UserSchemaODataProducer(this.cell, this); CountResponse cr = producer.getEntitiesCount(EntityType.EDM_TYPE_NAME, null); if (cr.getCount() > 0) { return false; } // Collectionに紐づくComplexTypeの一覧を取得する // ComplexTypeに紐づくリソース(ComplexTypeProperty)はComplexTypeが必ず親となる関係であるため // ComplexTypeのみ検索すれば、ComplexTypeに紐づくリソースまでチェックする必要はない cr = producer.getEntitiesCount(ComplexType.EDM_TYPE_NAME, null); return cr.getCount() < 1; } else if (DavCmp.TYPE_COL_SVC.equals(type)) { DavCmp svcSourceCol = this.getChild(SERVICE_SRC_COLLECTION); if (!svcSourceCol.exists()) { // クリティカルなタイミングでServiceコレクションが削除された場合 // ServiceSourceコレクションが存在しないため空とみなす return true; } return !(svcSourceCol.getChildrenCount() > 0); } DcCoreLog.Misc.UNREACHABLE_CODE_ERROR.writeLog(); throw DcCoreException.Server.UNKNOWN_ERROR; } @Override public void makeEmpty() { // TODO Impl } /** * @return Acl */ public Acl getAcl() { return this.acl; } /** * スキーマ認証のレベルを返す. * @return スキーマ認証レベル */ public String getConfidentialLevel() { if (acl == null) { return null; } return this.acl.getRequireSchemaAuthz(); } /** * ユニット昇格許可ユーザ設定を返す. * @return ユニット昇格許可ユーザ設定 */ public List<String> getOwnerRepresentativeAccounts() { return this.ownerRepresentativeAccounts; } /** * Boxをロックする. * @return 自ノードのロック */ public Lock lock() { log.debug("lock:" + LockKeyComposer.fullKeyFromCategoryAndKey(Lock.CATEGORY_DAV, null, this.box.getId(), null)); return LockManager.getLock(Lock.CATEGORY_DAV, null, this.box.getId(), null); } /** * @return ETag String with double quote signs. */ @Override public String getEtag() { StringBuilder sb = new StringBuilder("\""); sb.append(this.metaFile.getVersion()); sb.append("-"); sb.append(this.metaFile.getUpdated()); sb.append("\""); return sb.toString(); } /** * checks if this cmp is Cell level. * @return true if Cell level */ public boolean isCellLevel() { return false; } void createDir() { try { Files.createDirectories(this.fsDir.toPath()); } catch (IOException e) { // Failed to create directory. throw new RuntimeException(e); } } /** * returns if this resource exists.<br /> * before using this method, do not forget to load() and update the info. * @return true if this resource should exist */ @Override public final boolean exists() { return (this.fsDir != null) && this.fsDir.exists() && this.metaFile.exists(); } /** * load the info from FS for this Dav resouce. */ public final void load() { this.metaFile.load(); /* * Analyze JSON Object, and set metadata such as ACL. */ this.name = fsDir.getName(); this.acl = this.translateAcl(this.metaFile.getAcl()); @SuppressWarnings("unchecked") Map<String, String> props = (Map<String, String>) this.metaFile.getProperties(); if (props != null) { for (Map.Entry<String, String> entry : props.entrySet()) { String key = entry.getKey(); String val = entry.getValue(); int idx = key.indexOf("@"); String elementName = key.substring(0, idx); String namespace = key.substring(idx + 1); QName keyQName = new QName(namespace, elementName); Element element = parseProp(val); String elementNameSpace = element.getNamespaceURI(); // ownerRepresentativeAccountsの取り出し if (Key.PROP_KEY_OWNER_REPRESENTIVE_ACCOUNTS.equals(keyQName)) { NodeList accountNodeList = element.getElementsByTagNameNS(elementNameSpace, Key.PROP_KEY_OWNER_REPRESENTIVE_ACCOUNT.getLocalPart()); for (int i = 0; i < accountNodeList.getLength(); i++) { this.ownerRepresentativeAccounts.add(accountNodeList.item(i).getTextContent().trim()); } } } } } /** * Davの管理データ情報を最新化する.<br /> * 管理データが存在しない場合はエラーとする. */ public final void loadAndCheckDavInconsistency() { load(); if (this.metaFile == null) { // Boxから辿ってidで検索して、Davデータに不整合があった場合 throw DcCoreException.Dav.DAV_INCONSISTENCY_FOUND; } } private Element parseProp(String value) { // valをDOMでElement化 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = null; Document doc = null; try { builder = factory.newDocumentBuilder(); ByteArrayInputStream is = new ByteArrayInputStream(value.getBytes(CharEncoding.UTF_8)); doc = builder.parse(is); } catch (Exception e1) { throw DcCoreException.Dav.DAV_INCONSISTENCY_FOUND.reason(e1); } Element e = doc.getDocumentElement(); return e; } /* * proppatch メソッドへの対応. 保存の方式 key = namespaceUri + "@" + localName Value = * inner XML String */ @Override @SuppressWarnings("unchecked") public Multistatus proppatch(final Propertyupdate propUpdate, final String url) { long now = new Date().getTime(); String reqUri = url; Multistatus ms = this.of.createMultistatus(); Response res = this.of.createResponse(); res.getHref().add(reqUri); // Lock Lock lock = this.lock(); // 更新処理 try { this.load(); // ロック後の最新情報取得 if (!this.exists()) { // クリティカルなタイミング(初回ロード~ロック取得)で削除された場合は404エラーとする throw getNotFoundException().params(this.getUrl()); } Map<String, Object> propsJson = (Map<String, Object>) this.metaFile.getProperties(); List<Prop> propsToSet = propUpdate.getPropsToSet(); for (Prop prop : propsToSet) { if (null == prop) { throw DcCoreException.Dav.XML_CONTENT_ERROR; } List<Element> lpe = prop.getAny(); for (Element elem : lpe) { res.setProperty(elem, HttpStatus.SC_OK); String key = elem.getLocalName() + "@" + elem.getNamespaceURI(); String value = DcCoreUtils.nodeToString(elem); log.debug("key: " + key); log.debug("val: " + value); propsJson.put(key, value); } } List<Prop> propsToRemove = propUpdate.getPropsToRemove(); for (Prop prop : propsToRemove) { if (null == prop) { throw DcCoreException.Dav.XML_CONTENT_ERROR; } List<Element> lpe = prop.getAny(); for (Element elem : lpe) { String key = elem.getLocalName() + "@" + elem.getNamespaceURI(); String v = (String) propsJson.get(key); log.debug("Removing key: " + key); if (v == null) { res.setProperty(elem, HttpStatus.SC_NOT_FOUND); } else { propsJson.remove(key); res.setProperty(elem, HttpStatus.SC_OK); } } } // set the last updated date this.metaFile.setProperties((JSONObject) propsJson); this.metaFile.setUpdated(now); this.metaFile.save(); } finally { lock.release(); } ms.getResponse().add(res); return ms; } @Override public final ResponseBuilder acl(final Reader reader) { // リクエストが空でない場合、パースして適切な拡張を行う。 Acl aclToSet = null; try { aclToSet = ObjectIo.unmarshal(reader, Acl.class); } catch (Exception e1) { throw DcCoreException.Dav.XML_CONTENT_ERROR.reason(e1); } if (!aclToSet.validateAcl(isCellLevel())) { throw DcCoreException.Dav.XML_VALIDATE_ERROR; } // ロック Lock lock = this.lock(); try { // リソースのリロード this.load(); if (!this.exists()) { throw getNotFoundException().params(this.getUrl()); } // ACLのxml:baseの値を取得する String aclBase = aclToSet.getBase(); // principalのhref の値を ロール名(Name) を ロールID(__id) に変換する。 List<Ace> aceList = aclToSet.getAceList(); if (aceList != null) { for (Ace ace : aceList) { String pHref = ace.getPrincipalHref(); if (pHref != null) { String id = this.cell.roleResourceUrlToId(pHref, aclBase); ace.setPrincipalHref(id); } } } JSONParser parser = new JSONParser(); JSONObject aclJson = null; try { aclJson = (JSONObject) parser.parse(aclToSet.toJSON()); } catch (ParseException e) { throw DcCoreException.Dav.XML_ERROR.reason(e); } // ESへxm:baseの値を登録しない TODO これでいいのか? aclJson.remove(KEY_ACL_BASE); this.metaFile.setAcl(aclJson); this.metaFile.save(); // レスポンス return javax.ws.rs.core.Response.status(HttpStatus.SC_OK).header(HttpHeaders.ETAG, this.getEtag()); } finally { lock.release(); } } @Override public final ResponseBuilder putForCreate(final String contentType, final InputStream inputStream) { // Locking Lock lock = this.lock(); try { // 新規作成時には、作成対象のDavNodeは存在しないため、親DavNodeをリロードして存在確認する。 // 親DavNodeが存在しない場合:他のリクエストによって削除されたたため、404を返却 // 親DavNodeが存在するが、作成対象のDavNodeが存在する場合:他のリクエストによって作成されたたtめ、更新処理を実行 this.parent.load(); if (!this.parent.exists()) { throw DcCoreException.Dav.HAS_NOT_PARENT.params(this.parent.getUrl()); } // 作成対象のDavNodeが存在する場合は更新処理 if (this.exists()) { return this.doPutForUpdate(contentType, inputStream, null); } // 作成対象のDavNodeが存在しない場合は新規作成処理 return this.doPutForCreate(contentType, inputStream); } finally { // UNLOCK lock.release(); log.debug("unlock1"); } } @Override public final ResponseBuilder putForUpdate(final String contentType, final InputStream inputStream, String etag) { // ロック Lock lock = this.lock(); try { // 更新には、更新対象のDavNodeが存在するため、更新対象のDavNodeをリロードして存在確認する。 // 更新対象のDavNodeが存在しない場合: // ・更新対象の親DavNodeが存在しない場合:親ごと消えているため404を返却 // ・更新対象の親DavNodeが存在する場合:他のリクエストによって削除されたたため、作成処理を実行 // 更新対象のDavNodeが存在する場合:更新処理を実行 this.load(); if (this.metaFile == null) { this.parent.load(); if (this.parent.metaFile == null) { throw getNotFoundException().params(this.parent.getUrl()); } return this.doPutForCreate(contentType, inputStream); } return this.doPutForUpdate(contentType, inputStream, etag); } finally { // ロックを開放する lock.release(); log.debug("unlock2"); } } /* * newly create the resource */ final ResponseBuilder doPutForCreate(final String contentType, final InputStream inputStream) { // check the resource count checkChildResourceCount(); BufferedInputStream bufferedInput = new BufferedInputStream(inputStream); try { // create new directory. Files.createDirectory(Paths.get(this.fsPath)); // store the file content. File newFile = new File(this.getContentFilePath()); Files.copy(bufferedInput, newFile.toPath()); long writtenBytes = newFile.length(); // create new metadata file. this.metaFile = DavMetadataFile.prepareNewFile(this, DavCmp.TYPE_DAV_FILE); this.metaFile.setContentType(contentType); this.metaFile.setContentLength(writtenBytes); this.metaFile.save(); } catch (IOException ex) { throw DcCoreException.Dav.FS_INCONSISTENCY_FOUND.reason(ex); } this.isPhantom = false; return javax.ws.rs.core.Response.ok().status(HttpStatus.SC_CREATED).header(HttpHeaders.ETAG, this.getEtag()); } final ResponseBuilder doPutForUpdate(final String contentType, final InputStream inputStream, String etag) { // 現在時刻を取得 long now = new Date().getTime(); // 最新ノード情報をロード // TODO 全体として2回ロードしてしまうので、遅延ロードの仕組みを検討 this.load(); // クリティカルなタイミング(ロック~ロードまでの間)でWebDavの管理データが削除された場合の対応 // WebDavの管理データがこの時点で存在しない場合は404エラーとする if (!this.exists()) { throw getNotFoundException().params(this.getUrl()); } // 指定etagがあり、かつそれが*ではなく内部データから導出されるものと異なるときはエラー if (etag != null && !"*".equals(etag) && !this.getEtag().equals(etag)) { throw DcCoreException.Dav.ETAG_NOT_MATCH; } try { // Update Content BufferedInputStream bufferedInput = new BufferedInputStream(inputStream); File tmpFile = new File(this.getTempContentFilePath()); File contentFile = new File(this.getContentFilePath()); Files.copy(bufferedInput, tmpFile.toPath()); Files.delete(contentFile.toPath()); Files.move(tmpFile.toPath(), contentFile.toPath()); // Update Metadata this.metaFile.setUpdated(now); this.metaFile.setContentType(contentType); this.metaFile.setContentLength(contentFile.length()); this.metaFile.save(); } catch (IOException ex) { throw DcCoreException.Dav.FS_INCONSISTENCY_FOUND.reason(ex); } // response return javax.ws.rs.core.Response.ok().status(HttpStatus.SC_NO_CONTENT).header(HttpHeaders.ETAG, this.getEtag()); } @Override public final ResponseBuilder get(final String rangeHeaderField) { String contentType = this.getContentType(); ResponseBuilder res = null; String fileFullPath = this.fsPath + File.separator + CONTENT_FILE_NAME; final long fileSize = this.getContentLength(); // Rangeヘッダ解析処理 final RangeHeaderHandler range = RangeHeaderHandler.parse(rangeHeaderField, fileSize); try { // Rangeヘッダ指定の時とで処理の切り分け if (!range.isValid()) { // ファイル全体返却 StreamingOutput sout = new StreamingOutputForDavFile(fileFullPath); res = davFileResponse(sout, fileSize, contentType); } else { // Range対応部分レスポンス // Rangeヘッダの範囲チェック if (!range.isSatisfiable()) { DcCoreLog.Dav.REQUESTED_RANGE_NOT_SATISFIABLE.params(range.getRangeHeaderField()).writeLog(); throw DcCoreException.Dav.REQUESTED_RANGE_NOT_SATISFIABLE; } if (range.getByteRangeSpecCount() > 1) { // MultiPartレスポンスには未対応 throw DcCoreException.Misc.NOT_IMPLEMENTED.params("Range-MultiPart"); } else { StreamingOutput sout = new StreamingOutputForDavFileWithRange(fileFullPath, fileSize, range); res = davFileResponseForRange(sout, fileSize, contentType, range); } } return res.header(HttpHeaders.ETAG, this.getEtag()).header(DcCoreUtils.HttpHeaders.ACCEPT_RANGES, RangeHeaderHandler.BYTES_UNIT); } catch (BinaryDataNotFoundException nex) { this.load(); if (!this.exists()) { throw getNotFoundException().params(this.getUrl()); } throw DcCoreException.Dav.DAV_UNAVAILABLE.reason(nex); } } /** * ファイルレスポンス処理. * @param sout * StreamingOuputオブジェクト * @param fileSize * ファイルサイズ * @param contentType * コンテントタイプ * @return レスポンス */ public ResponseBuilder davFileResponse(final StreamingOutput sout, long fileSize, String contentType) { return javax.ws.rs.core.Response.ok(sout).header(HttpHeaders.CONTENT_LENGTH, fileSize) .header(HttpHeaders.CONTENT_TYPE, contentType); } /** * ファイルレスポンス処理. * @param sout * StreamingOuputオブジェクト * @param fileSize * ファイルサイズ * @param contentType * コンテントタイプ * @param range * RangeHeaderHandler * @return レスポンス */ private ResponseBuilder davFileResponseForRange(final StreamingOutput sout, long fileSize, String contentType, final RangeHeaderHandler range) { // MultiPartには対応しないため1個目のbyte-renge-setだけ処理する。 int rangeIndex = 0; List<ByteRangeSpec> brss = range.getByteRangeSpecList(); final ByteRangeSpec brs = brss.get(rangeIndex); // iPadのsafariにおいてChunkedのRangeレスポンスを処理できなかったので明にContent-Lengthを返却している。 return javax.ws.rs.core.Response.status(HttpStatus.SC_PARTIAL_CONTENT).entity(sout) .header(DcCoreUtils.HttpHeaders.CONTENT_RANGE, brs.makeContentRangeHeaderField()) .header(HttpHeaders.CONTENT_LENGTH, brs.getContentLength()) .header(HttpHeaders.CONTENT_TYPE, contentType); } @Override public final String getName() { return this.name; } @Override public final DavCmp getChild(final String childName) { // if self is phantom then all children should be phantom. if (this.isPhantom) { return DavCmpFsImpl.createPhantom(childName, this); } // otherwise, child might / might not be phantom. return DavCmpFsImpl.create(childName, this); } @Override public String getType() { if (this.isPhantom) { return DavCmp.TYPE_NULL; } if (this.metaFile == null) { return DavCmp.TYPE_NULL; } return (String) this.metaFile.getNodeType(); } @Override public final ResponseBuilder mkcol(final String type) { if (!this.isPhantom) { throw new RuntimeException("Bug do not call this ."); } // ロック Lock lock = this.lock(); try { // ここで改めて存在確認が必要。 // TODO 何等かの手段で、再ロード this.parent.load(); if (!this.parent.exists()) { // クリティカルなタイミングで先に親を削除されてしまい、 // 親が存在しないので409エラーとする throw DcCoreException.Dav.HAS_NOT_PARENT.params(this.parent.getUrl()); } if (this.exists()) { // クリティカルなタイミングで先にコレクションを作られてしまい、 // すでに存在するのでEXCEPTION throw DcCoreException.Dav.METHOD_NOT_ALLOWED; } // コレクションの階層数のチェック DavCmpFsImpl current = this; int depth = 0; int maxDepth = DcCoreConfig.getMaxCollectionDepth(); while (null != current.parent) { current = current.parent; depth++; } if (depth > maxDepth) { // コレクション数の制限を超えたため、400エラーとする throw DcCoreException.Dav.COLLECTION_DEPTH_ERROR; } // 親コレクション内のコレクション・ファイル数のチェック checkChildResourceCount(); // Create New Directory Files.createDirectory(this.fsDir.toPath()); // Create New Meta File this.metaFile = DavMetadataFile.prepareNewFile(this, type); this.metaFile.save(); // TODO ディレクトリとメタデータつくるだけでいい? } catch (IOException e) { throw new RuntimeException(e); } finally { // UNLOCK lock.release(); log.debug("unlock"); } this.isPhantom = false; // Response return javax.ws.rs.core.Response.status(HttpStatus.SC_CREATED).header(HttpHeaders.ETAG, this.getEtag()); } /** * process MOVE operation. * @param etag * ETag Value * @param overwrite * whether or not overwrite the target resource * @param davDestination * Destination information. * @return ResponseBuilder Response Object */ @Override public ResponseBuilder move(String etag, String overwrite, DavDestination davDestination) { ResponseBuilder res = null; // ロック Lock lock = this.lock(); try { // 移動元リソースの存在チェック this.load(); if (!this.exists()) { // クリティカルなタイミング(初回ロード~ロック取得)で移動元を削除された場合。 // 移動元が存在しないため404エラーとする throw getNotFoundException().params(this.getUrl()); } // 指定etagがあり、かつそれが*ではなく内部データから導出されるものと異なるときはエラー if (etag != null && !"*".equals(etag) && !this.getEtag().equals(etag)) { throw DcCoreException.Dav.ETAG_NOT_MATCH; } // 移動元のDavNodeをリロードしたことにより親DavNodeが別のリソースに切り替わっている可能性があるため、リロードする。 // この際、親DavNodeが削除されている可能性もあるため、存在チェックを実施する。 // this.parent.nodeId = this.metaFile.getParentId(); // this.parent.load(); // if (this.parent.metaFile == null) { // throw getNotFoundException().params(this.parent.getUrl()); // } // 移動先のロード davDestination.loadDestinationHierarchy(); // 移動先のバリデート davDestination.validateDestinationResource(overwrite, this); // MOVEメソッドでは移動元と移動先のBoxが同じであるため、移動先のアクセスコンテキストを取得しても、 // 移動元のアクセスコンテキストを取得しても同じObjectが取得できる // このため、移動先のアクセスコンテキストを用いている AccessContext ac = davDestination.getDestinationRsCmp().getAccessContext(); // 移動先に対するアクセス制御 // 以下の理由により、ロック後に移動先に対するアクセス制御を行うこととした。 // 1.アクセス制御ではESへのアクセスは発生しないため、ロック中に実施してもロック期間の長さに与える影響は少ない。 // 2.ロック前に移動先のアクセス制御を行う場合、移動先の情報を取得する必要があり、ESへのリクエストが発生するため。 davDestination.getDestinationRsCmp().getParent().checkAccessContext(ac, BoxPrivilege.WRITE); File destDir = ((DavCmpFsImpl) davDestination.getDestinationCmp()).fsDir; if (!davDestination.getDestinationCmp().exists()) { Files.move(this.fsDir.toPath(), destDir.toPath()); res = javax.ws.rs.core.Response.status(HttpStatus.SC_CREATED); } else { FileUtils.deleteDirectory(destDir); Files.move(this.fsDir.toPath(), destDir.toPath(), StandardCopyOption.REPLACE_EXISTING); res = javax.ws.rs.core.Response.status(HttpStatus.SC_NO_CONTENT); } } catch (IOException e) { throw new RuntimeException(e); } finally { // UNLOCK lock.release(); log.debug("unlock"); } res.header(HttpHeaders.LOCATION, davDestination.getDestinationUri()); res.header(HttpHeaders.ETAG, this.getEtag()); return res; } private void checkChildResourceCount() { // 親コレクション内のコレクション・ファイル数のチェック int maxChildResource = DcCoreConfig.getMaxChildResourceCount(); if (this.parent.getChildrenCount() >= maxChildResource) { // コレクション内に作成可能なコレクション・ファイル数の制限を超えたため、400エラーとする throw DcCoreException.Dav.COLLECTION_CHILDRESOURCE_ERROR; } } @Override public final ResponseBuilder linkChild(final String childName, final String childNodeId, final Long asof) { return null; } @Override public final ResponseBuilder unlinkChild(final String childName, final Long asof) { return null; } /** * delete this resource. * @param ifMatch ifMatch header * @param recursive bool * @return JaxRS応答オブジェクトビルダ */ @Override public final ResponseBuilder delete(final String ifMatch, boolean recursive) { // 指定etagがあり、かつそれが*ではなく内部データから導出されるものと異なるときはエラー if (ifMatch != null && !"*".equals(ifMatch) && !this.getEtag().equals(ifMatch)) { throw DcCoreException.Dav.ETAG_NOT_MATCH; } // ロック Lock lock = this.lock(); try { // リロード this.load(); if (this.metaFile == null) { throw getNotFoundException().params(this.getUrl()); } if (!recursive) { // WebDAVコレクションであって子孫リソースがあったら、エラーとする if (TYPE_COL_WEBDAV.equals(this.getType()) && this.getChildrenCount() > 0) { throw DcCoreException.Dav.HAS_CHILDREN; } } else { // TODO impl recursive throw DcCoreException.Misc.NOT_IMPLEMENTED; } this.doDelete(); } finally { // ★LOCK log.debug("unlock"); lock.release(); } return javax.ws.rs.core.Response.ok().status(HttpStatus.SC_NO_CONTENT); } private void doDelete() { try { FileUtils.deleteDirectory(this.fsDir); } catch (IOException e) { throw DcCoreException.Dav.FS_INCONSISTENCY_FOUND.reason(e); } } /** * バイナリデータのアクセサのインスタンスを生成して返す. * @return アクセサのインスタンス */ protected BinaryDataAccessor getBinaryDataAccessor() { String owner = cell.getOwner(); String unitUserName = null; if (owner == null) { unitUserName = AccessContext.TYPE_ANONYMOUS; } else { unitUserName = IndexNameEncoder.encodeEsIndexName(owner); } return new BinaryDataAccessor(DcCoreConfig.getBlobStoreRoot(), unitUserName, DcCoreConfig.getPhysicalDeleteMode(), DcCoreConfig.getFsyncEnabled()); } @Override public final DavCmp getParent() { return this.parent; } @Override public final DcODataProducer getODataProducer() { return ModelFactory.ODataCtl.userData(this.cell, this); } @Override public final DcODataProducer getSchemaODataProducer(Cell cellObject) { return ModelFactory.ODataCtl.userSchema(cellObject, this); } @Override public final int getChildrenCount() { return this.getChildDir().length; } @Override public Map<String, DavCmp> getChildren() { Map<String, DavCmp> ret = new HashMap<>(); File[] files = this.getChildDir(); for (File f : files) { String childName = f.getName(); ret.put(childName, this.getChild(childName)); } return ret; } /* * retrieve child resource dir. */ private File[] getChildDir() { File[] children = this.fsDir.listFiles(new FileFilter() { @Override public boolean accept(File child) { if (child.isDirectory()) { return true; } return false; } }); return children; } private Acl translateAcl(JSONObject aclObj) { // principalのhref の値を ロールID(__id)からロールリソースURLに変換する。 // base:xml値の設定 String baseUrlStr = createBaseUrlStr(); // TODO これはES検索が何度も走る重い処理であるため、必要になってはじめてやるべき // ここからは、一旦、はずすべきか。 return this.roleIdToName(aclObj, baseUrlStr); } /** * ロールIDからロールリソースURLを取得. * jsonObjのロールIDをロールリソースURLに置換する * @param jsonObj * ID置換後のJSON * @param baseUrlStr * xml:base値 */ private Acl roleIdToName(Object jsonObj, String baseUrlStr) { Acl ret = Acl.fromJson(((JSONObject) jsonObj).toJSONString()); List<Ace> aceList = ret.getAceList(); if (aceList == null) { return ret; } // xml:base対応 List<Ace> eraseList = new ArrayList<>(); for (Ace ace : aceList) { String pHref = ace.getPrincipalHref(); if (pHref != null) { // ロールIDに該当するロール名が無かった場合はロールが削除済みと判断し、無視する。 String roloResourceUrl = this.cell.roleIdToRoleResourceUrl(pHref); log.debug("###" + pHref + ":" + roloResourceUrl); if (roloResourceUrl == null) { eraseList.add(ace); continue; } // base:xml値からロールリソースURLの編集 roloResourceUrl = baseUrlToRoleResourceUrl(baseUrlStr, roloResourceUrl); ace.setPrincipalHref(roloResourceUrl); } } aceList.removeAll(eraseList); ret.setBase(baseUrlStr); return ret; } /** * PROPFINDのACL内のxml:base値を生成します. * @return */ private String createBaseUrlStr() { String result = null; if (this.box != null) { // Boxレベル以下のACLの場合、BoxリソースのURL // セルURLは連結でスラッシュつけてるので、URLの最後がスラッシュだったら消す。 result = String.format(Role.ROLE_RESOURCE_FORMAT, this.cell.getUrl().replaceFirst("/$", ""), this.box.getName(), ""); } else { // CellレベルのACLの場合、デフォルトBoxのリソースURL // セルURLは連結でスラッシュつけてるので、URLの最後がスラッシュだったら消す。 result = String.format(Role.ROLE_RESOURCE_FORMAT, this.cell.getUrl().replaceFirst("/$", ""), Box.DEFAULT_BOX_NAME, ""); } return result; } /** * xml:baseに従ってRoleResorceUrlの整形. * @param baseUrlStr * xml:baseの値 * @param roloResourceUrl * ロールリソースURL * @return */ private String baseUrlToRoleResourceUrl(String baseUrlStr, String roloResourceUrlStr) { String result = null; Role baseUrl = null; Role roloResourceUrl = null; try { // base:xmlはロールリソースURLではないため、ダミーで「__」を追加 baseUrl = new Role(new URL(baseUrlStr + "__")); roloResourceUrl = new Role(new URL(roloResourceUrlStr)); } catch (MalformedURLException e) { throw DcCoreException.Dav.ROLE_NOT_FOUND.reason(e); } if (baseUrl.getBoxName().equals(roloResourceUrl.getBoxName())) { // base:xmlのBOXとロールリソースURLのBOXが同じ場合 result = roloResourceUrl.getName(); } else { // base:xmlのBOXとロールリソースURLのBOXが異なる場合 result = String.format(ACL_RELATIVE_PATH_FORMAT, roloResourceUrl.getBoxName(), roloResourceUrl.getName()); } return result; } static final String KEY_SCHEMA = "Schema"; static final String KEY_ACL_BASE = "@base"; static final String ACL_RELATIVE_PATH_FORMAT = "../%s/%s"; /** * @return cell id */ public String getCellId() { return this.cell.getId(); } /** * @return DavMetadataFile */ public DavMetadataFile getDavMetadataFile() { return this.metaFile; } /** * @return FsPath */ public String getFsPath() { return this.fsPath; } private String getContentFilePath() { return this.fsPath + File.separator + CONTENT_FILE_NAME; } private String getTempContentFilePath() { return this.fsPath + File.separator + TEMP_FILE_NAME; } /** * @return URL string of this Dav node. */ public String getUrl() { // go to the top ancestor DavCmp (BoxCmp) recursively, and BoxCmp // overrides here and give root url. return this.parent.getUrl() + "/" + this.name; } /** * retruns NotFoundException for this resource. <br /> * messages should vary among resource type Cell, box, file, etc.. * Each *Cmp class should override this method and define the proper exception <br /> * Additional info (reason etc.) for the message should be set after calling this method. * @return NotFoundException */ public DcCoreException getNotFoundException() { return DcCoreException.Dav.RESOURCE_NOT_FOUND; } @Override public Cell getCell() { return this.cell; } @Override public Box getBox() { return this.box; } @Override public Long getUpdated() { return this.metaFile.getUpdated(); } @Override public Long getPublished() { return this.metaFile.getPublished(); } @Override public Long getContentLength() { return this.metaFile.getContentLength(); } @Override public String getContentType() { return this.metaFile.getContentType(); } @Override public String getId() { return this.metaFile.getNodeId(); } @Override @SuppressWarnings("unchecked") public Map<String, String> getProperties() { return this.metaFile.getProperties(); } }