001/* 002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nicolas Chapurlat <[email protected]> 018 */ 019 020package org.nuxeo.ecm.core.model; 021 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.function.BiConsumer; 028 029import org.apache.commons.lang3.mutable.MutableBoolean; 030import org.apache.commons.lang3.mutable.MutableObject; 031import org.nuxeo.ecm.core.api.CloseableCoreSession; 032import org.nuxeo.ecm.core.api.CoreInstance; 033import org.nuxeo.ecm.core.api.CoreSession; 034import org.nuxeo.ecm.core.api.DocumentModel; 035import org.nuxeo.ecm.core.api.DocumentRef; 036import org.nuxeo.ecm.core.api.IdRef; 037import org.nuxeo.ecm.core.api.PathRef; 038import org.nuxeo.ecm.core.api.local.LocalException; 039import org.nuxeo.ecm.core.schema.types.resolver.AbstractObjectResolver; 040import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 041 042/** 043 * This {@link ObjectResolver} allows to manage integrity for fields containing {@link DocumentModel} references (id or 044 * path). 045 * <p> 046 * Resolved references must be either a path or an id, default mode is id. Storing path keep link with place in the 047 * Document hierarchy no matter which Document is referenced. Storing id track the Document no matter where the Document 048 * is stored. 049 * </p> 050 * <p> 051 * All references, id or path, are prefixed with the document expected repository name. For example : 052 * </p> 053 * <ul> 054 * <li>default:352c21bc-f908-4507-af99-411d3d84ee7d</li> 055 * <li>test:/path/to/my/doc</li> 056 * </ul> 057 * <p> 058 * The {@link #fetch(Object)} method returns {@link DocumentModel}. The {@link #fetch(Class, Object)} returns 059 * {@link DocumentModel} or specific document adapter. 060 * </p> 061 * <p> 062 * To use it, put the following code in your schema XSD : 063 * </p> 064 * 065 * <pre> 066 * {@code 067 * <!-- default resolver is an id based resolver --> 068 * <xs:simpleType name="favoriteDocument1"> 069 * <xs:restriction base="xs:string" ref:resolver="documentResolver" /> 070 * </xs:simpleType> 071 * 072 * <!-- store id --> 073 * <xs:simpleType name="favoriteDocument2"> 074 * <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" /> 075 * </xs:simpleType> 076 * 077 * <!-- store path --> 078 * <xs:simpleType name="bestDocumentRepositoryPlace"> 079 * <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" /> 080 * </xs:simpleType> 081 * } 082 * </pre> 083 * 084 * @since 7.1 085 */ 086public class DocumentModelResolver extends AbstractObjectResolver implements ObjectResolver { 087 088 private static final long serialVersionUID = 1L; 089 090 public static final String NAME = "documentResolver"; 091 092 public static final String PARAM_STORE = "store"; 093 094 public static final String STORE_REPO_AND_PATH = "path"; 095 096 /** Since 10.2 */ 097 public static final String STORE_PATH_ONLY = "pathOnly"; 098 099 public static final String STORE_REPO_AND_ID = "id"; 100 101 /** Since 10.2 */ 102 public static final String STORE_ID_ONLY = "idOnly"; 103 104 public enum MODE { 105 /** Reference is a path prefixed with a repository. */ 106 REPO_AND_PATH_REF, 107 /** Reference is an id prefixed with a repository. */ 108 REPO_AND_ID_REF, 109 /** Reference is a path. */ 110 PATH_ONLY_REF, 111 /** Reference is an id. */ 112 ID_ONLY_REF, 113 } 114 115 private MODE mode = MODE.REPO_AND_ID_REF; 116 117 public MODE getMode() { 118 return mode; 119 } 120 121 private List<Class<?>> managedClasses = null; 122 123 @Override 124 public List<Class<?>> getManagedClasses() { 125 if (managedClasses == null) { 126 managedClasses = new ArrayList<>(); 127 managedClasses.add(DocumentModel.class); 128 } 129 return managedClasses; 130 } 131 132 @Override 133 public void configure(Map<String, String> parameters) throws IllegalStateException { 134 super.configure(parameters); 135 String store = parameters.get(PARAM_STORE); 136 if (store == null) { 137 store = ""; // use default 138 } 139 switch (store) { 140 case STORE_PATH_ONLY: 141 mode = MODE.PATH_ONLY_REF; 142 break; 143 case STORE_ID_ONLY: 144 mode = MODE.ID_ONLY_REF; 145 break; 146 case STORE_REPO_AND_PATH: 147 mode = MODE.REPO_AND_PATH_REF; 148 break; 149 case STORE_REPO_AND_ID: 150 default: 151 mode = MODE.REPO_AND_ID_REF; 152 store = STORE_REPO_AND_ID; 153 break; 154 } 155 this.parameters.put(PARAM_STORE, store); 156 } 157 158 @Override 159 public String getName() { 160 checkConfig(); 161 return NAME; 162 } 163 164 @Override 165 public boolean validate(Object value) { 166 return validate(value, null); 167 } 168 169 @Override 170 public boolean validate(Object value, Object context) { 171 MutableBoolean validated = new MutableBoolean(); 172 resolve(value, context, (session, docRef) -> { 173 if (session.exists(docRef)) { 174 validated.setTrue(); 175 } 176 }); 177 return validated.isTrue(); 178 } 179 180 @Override 181 public Object fetch(Object value) { 182 return fetch(value, null); 183 } 184 185 @Override 186 public Object fetch(Object value, Object context) { 187 MutableObject<DocumentModel> docHolder = new MutableObject<>(); 188 resolve(value, context, (session, docRef) -> { 189 if (session.exists(docRef)) { 190 DocumentModel doc = session.getDocument(docRef); 191 // detach because we're about to close the session 192 doc.detach(true); 193 docHolder.setValue(doc); 194 } 195 }); 196 return docHolder.getValue(); 197 } 198 199 /** 200 * Resolves the value (in the context) into a session and docRef, and passes them to the resolver. 201 * <p> 202 * The resolver is not called if the value cannot be resolved. 203 */ 204 protected void resolve(Object value, Object context, BiConsumer<CoreSession, DocumentRef> resolver) { 205 checkConfig(); 206 if (!(value instanceof String)) { 207 return; 208 } 209 REF ref = REF.fromValue((String) value); 210 if (ref == null) { 211 return; 212 } 213 CloseableCoreSession closeableCoreSession = null; 214 try { 215 CoreSession session; 216 try { 217 if (ref.repo != null) { 218 // we have an explicit repository name 219 if (context != null && ref.repo.equals(((CoreSession) context).getRepositoryName())) { 220 // if it's the same repository as the context session, use it directly 221 session = (CoreSession) context; 222 } else { 223 // otherwise open a new one 224 closeableCoreSession = CoreInstance.openCoreSession(ref.repo); 225 session = closeableCoreSession; 226 } 227 } else { 228 // use session from context 229 session = (CoreSession) context; 230 if (session == null) { 231 // use the default repository if none is provided in the context 232 closeableCoreSession = CoreInstance.openCoreSession(null); 233 session = closeableCoreSession; 234 } 235 } 236 } catch (LocalException e) { 237 // no such repository 238 return; 239 } 240 DocumentRef docRef; 241 switch (mode) { 242 case ID_ONLY_REF: 243 case REPO_AND_ID_REF: 244 docRef = new IdRef(ref.ref); 245 break; 246 case PATH_ONLY_REF: 247 case REPO_AND_PATH_REF: 248 docRef = new PathRef(ref.ref); 249 break; 250 default: 251 // unknown ref type 252 return; 253 } 254 resolver.accept(session, docRef); 255 } finally { 256 if (closeableCoreSession != null) { 257 closeableCoreSession.close(); 258 } 259 } 260 } 261 262 @Override 263 public <T> T fetch(Class<T> type, Object value) throws IllegalStateException { 264 checkConfig(); 265 DocumentModel doc = (DocumentModel) fetch(value); 266 if (doc != null) { 267 if (type.isInstance(doc)) { 268 return type.cast(doc); 269 } 270 return doc.getAdapter(type); 271 } 272 return null; 273 } 274 275 @Override 276 public Serializable getReference(Object entity) throws IllegalStateException { 277 checkConfig(); 278 if (entity instanceof DocumentModel) { 279 DocumentModel doc = (DocumentModel) entity; 280 switch (mode) { 281 case ID_ONLY_REF: 282 return doc.getId(); 283 case PATH_ONLY_REF: 284 return doc.getPath().toString(); 285 case REPO_AND_ID_REF: 286 return doc.getRepositoryName() + ":" + doc.getId(); 287 case REPO_AND_PATH_REF: 288 return doc.getRepositoryName() + ":" + doc.getPath().toString(); 289 } 290 } 291 return null; 292 } 293 294 @Override 295 public String getConstraintErrorMessage(Object invalidValue, Locale locale) { 296 checkConfig(); 297 switch (mode) { 298 case ID_ONLY_REF: 299 case REPO_AND_ID_REF: 300 return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale); 301 case PATH_ONLY_REF: 302 case REPO_AND_PATH_REF: 303 return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale); 304 default: 305 return String.format("%s cannot resolve reference %s", getName(), invalidValue); 306 } 307 } 308 309 310 protected static final class REF { 311 312 protected String repo; 313 314 protected String ref; 315 316 protected REF() { 317 } 318 319 protected static REF fromValue(String value) { 320 String[] split = value.split(":"); 321 if (split.length == 1) { 322 REF ref = new REF(); 323 ref.repo = null; // caller will use context session, or the default repo 324 ref.ref = split[0]; 325 return ref; 326 } 327 if (split.length == 2) { 328 REF ref = new REF(); 329 ref.repo = split[0]; 330 ref.ref = split[1]; 331 return ref; 332 } 333 return null; 334 } 335 336 } 337 338}