001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nicolas Chapurlat <nchapurlat@nuxeo.com> 016 */ 017 018package org.nuxeo.ecm.core.model; 019 020import java.io.Serializable; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027 028import org.nuxeo.ecm.core.api.CoreInstance; 029import org.nuxeo.ecm.core.api.CoreSession; 030import org.nuxeo.ecm.core.api.DocumentModel; 031import org.nuxeo.ecm.core.api.DocumentNotFoundException; 032import org.nuxeo.ecm.core.api.IdRef; 033import org.nuxeo.ecm.core.api.PathRef; 034import org.nuxeo.ecm.core.api.local.LocalException; 035import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 036 037/** 038 * This {@link ObjectResolver} allows to manage integrity for fields containing {@link DocumentModel} references (id or 039 * path). 040 * <p> 041 * Resolved references must be either a path or an id, default mode is id. Storing path keep link with place in the 042 * Document hierarchy no matter which Document is referenced. Storing id track the Document no matter where the Document 043 * is stored. 044 * </p> 045 * <p> 046 * All references, id or path, are prefixed with the document expected repository name. For example : 047 * </p> 048 * <ul> 049 * <li>default:352c21bc-f908-4507-af99-411d3d84ee7d</li> 050 * <li>test:/path/to/my/doc</li> 051 * </ul> 052 * <p> 053 * The {@link #fetch(Object)} method returns {@link DocumentModel}. The {@link #fetch(Class, Object)} returns 054 * {@link DocumentModel} or specific document adapter. 055 * </p> 056 * <p> 057 * To use it, put the following code in your schema XSD : 058 * </p> 059 * 060 * <pre> 061 * {@code 062 * <!-- default resolver is an id based resolver --> 063 * <xs:simpleType name="favoriteDocument1"> 064 * <xs:restriction base="xs:string" ref:resolver="documentResolver" /> 065 * </xs:simpleType> 066 * 067 * <!-- store id --> 068 * <xs:simpleType name="favoriteDocument2"> 069 * <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" /> 070 * </xs:simpleType> 071 * 072 * <!-- store path --> 073 * <xs:simpleType name="bestDocumentRepositoryPlace"> 074 * <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" /> 075 * </xs:simpleType> 076 * } 077 * </pre> 078 * 079 * @since 7.1 080 */ 081public class DocumentModelResolver implements ObjectResolver { 082 083 private static final long serialVersionUID = 1L; 084 085 private static final String DEFAULT_REPO_NAME = "default"; 086 087 public static final String NAME = "documentResolver"; 088 089 public static final String PARAM_STORE = "store"; 090 091 public static final String STORE_PATH_REF = "path"; 092 093 public static final String STORE_ID_REF = "id"; 094 095 private Map<String, Serializable> parameters; 096 097 public static enum MODE { 098 PATH_REF, ID_REF; 099 } 100 101 private MODE mode = MODE.ID_REF; 102 103 public MODE getMode() { 104 return mode; 105 } 106 107 private List<Class<?>> managedClasses = null; 108 109 @Override 110 public List<Class<?>> getManagedClasses() { 111 if (managedClasses == null) { 112 managedClasses = new ArrayList<Class<?>>(); 113 managedClasses.add(DocumentModel.class); 114 } 115 return managedClasses; 116 } 117 118 @Override 119 public void configure(Map<String, String> parameters) throws IllegalStateException { 120 if (this.parameters != null) { 121 throw new IllegalStateException("cannot change configuration, may be already in use somewhere"); 122 } 123 String store = parameters.get(PARAM_STORE); 124 if (store != null) { 125 if (STORE_ID_REF.equals(store)) { 126 mode = MODE.ID_REF; 127 } else if (STORE_PATH_REF.equals(store)) { 128 mode = MODE.PATH_REF; 129 } 130 } 131 this.parameters = new HashMap<String, Serializable>(); 132 this.parameters.put(PARAM_STORE, mode == MODE.ID_REF ? STORE_ID_REF : STORE_PATH_REF); 133 } 134 135 @Override 136 public String getName() { 137 checkConfig(); 138 return NAME; 139 } 140 141 @Override 142 public Map<String, Serializable> getParameters() { 143 checkConfig(); 144 return Collections.unmodifiableMap(parameters); 145 } 146 147 @Override 148 public boolean validate(Object value) throws IllegalStateException { 149 checkConfig(); 150 if (value != null && value instanceof String) { 151 REF ref = REF.fromValue((String) value); 152 if (ref != null) { 153 try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) { 154 switch (mode) { 155 case ID_REF: 156 return session.exists(new IdRef(ref.ref)); 157 case PATH_REF: 158 return session.exists(new PathRef(ref.ref)); 159 } 160 } catch (LocalException le) { // no such repo 161 return false; 162 } 163 } 164 } 165 return false; 166 } 167 168 @Override 169 public Object fetch(Object value) throws IllegalStateException { 170 checkConfig(); 171 if (value != null && value instanceof String) { 172 REF ref = REF.fromValue((String) value); 173 if (ref != null) { 174 try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) { 175 try { 176 switch (mode) { 177 case ID_REF: 178 return session.getDocument(new IdRef(ref.ref)); 179 case PATH_REF: 180 return session.getDocument(new PathRef(ref.ref)); 181 } 182 } catch (DocumentNotFoundException e) { 183 return null; 184 } 185 } catch (LocalException le) { // no such repo 186 return null; 187 } 188 } 189 } 190 return null; 191 } 192 193 @Override 194 public <T> T fetch(Class<T> type, Object value) throws IllegalStateException { 195 checkConfig(); 196 DocumentModel doc = (DocumentModel) fetch(value); 197 if (doc != null) { 198 if (type.isInstance(doc)) { 199 return type.cast(doc); 200 } 201 return doc.getAdapter(type); 202 } 203 return null; 204 } 205 206 @Override 207 public Serializable getReference(Object entity) throws IllegalStateException { 208 checkConfig(); 209 if (entity != null && entity instanceof DocumentModel) { 210 DocumentModel doc = (DocumentModel) entity; 211 String repositoryName = doc.getRepositoryName(); 212 if (repositoryName != null) { 213 switch (mode) { 214 case ID_REF: 215 return repositoryName + ":" + doc.getId(); 216 case PATH_REF: 217 return repositoryName + ":" + doc.getPath().toString(); 218 } 219 } 220 } 221 return null; 222 } 223 224 @Override 225 public String getConstraintErrorMessage(Object invalidValue, Locale locale) { 226 checkConfig(); 227 switch (mode) { 228 case ID_REF: 229 return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale); 230 case PATH_REF: 231 return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale); 232 default: 233 return String.format("%s cannot resolve reference %s", getName(), invalidValue); 234 } 235 } 236 237 private void checkConfig() throws IllegalStateException { 238 if (parameters == null) { 239 throw new IllegalStateException( 240 "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration."); 241 } 242 } 243 244 protected static final class REF { 245 246 protected String repo; 247 248 protected String ref; 249 250 protected REF() { 251 } 252 253 protected static REF fromValue(String value) { 254 String[] split = value.split(":"); 255 if (split.length == 1) { 256 REF ref = new REF(); 257 ref.repo = DEFAULT_REPO_NAME; 258 ref.ref = split[0]; 259 return ref; 260 } 261 if (split.length == 2) { 262 REF ref = new REF(); 263 ref.repo = split[0]; 264 ref.ref = split[1]; 265 return ref; 266 } 267 return null; 268 } 269 270 } 271 272}