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