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 String DEFAULT_REPO_NAME = "default"; 084 085 public static final String NAME = "documentResolver"; 086 087 public static final String PARAM_STORE = "store"; 088 089 public static final String STORE_PATH_REF = "path"; 090 091 public static final String STORE_ID_REF = "id"; 092 093 private Map<String, Serializable> parameters; 094 095 public static enum MODE { 096 PATH_REF, ID_REF; 097 } 098 099 private MODE mode = MODE.ID_REF; 100 101 public MODE getMode() { 102 return mode; 103 } 104 105 private List<Class<?>> managedClasses = null; 106 107 @Override 108 public List<Class<?>> getManagedClasses() { 109 if (managedClasses == null) { 110 managedClasses = new ArrayList<Class<?>>(); 111 managedClasses.add(DocumentModel.class); 112 } 113 return managedClasses; 114 } 115 116 @Override 117 public void configure(Map<String, String> parameters) throws IllegalStateException { 118 if (this.parameters != null) { 119 throw new IllegalStateException("cannot change configuration, may be already in use somewhere"); 120 } 121 String store = parameters.get(PARAM_STORE); 122 if (store != null) { 123 if (STORE_ID_REF.equals(store)) { 124 mode = MODE.ID_REF; 125 } else if (STORE_PATH_REF.equals(store)) { 126 mode = MODE.PATH_REF; 127 } 128 } 129 this.parameters = new HashMap<String, Serializable>(); 130 this.parameters.put(PARAM_STORE, mode == MODE.ID_REF ? STORE_ID_REF : STORE_PATH_REF); 131 } 132 133 @Override 134 public String getName() { 135 checkConfig(); 136 return NAME; 137 } 138 139 @Override 140 public Map<String, Serializable> getParameters() { 141 checkConfig(); 142 return Collections.unmodifiableMap(parameters); 143 } 144 145 @Override 146 public boolean validate(Object value) throws IllegalStateException { 147 checkConfig(); 148 if (value != null && value instanceof String) { 149 REF ref = REF.fromValue((String) value); 150 if (ref != null) { 151 try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) { 152 switch (mode) { 153 case ID_REF: 154 return session.exists(new IdRef(ref.ref)); 155 case PATH_REF: 156 return session.exists(new PathRef(ref.ref)); 157 } 158 } catch (LocalException le) { // no such repo 159 return false; 160 } 161 } 162 } 163 return false; 164 } 165 166 @Override 167 public Object fetch(Object value) throws IllegalStateException { 168 checkConfig(); 169 if (value != null && value instanceof String) { 170 REF ref = REF.fromValue((String) value); 171 if (ref != null) { 172 try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) { 173 try { 174 switch (mode) { 175 case ID_REF: 176 return session.getDocument(new IdRef(ref.ref)); 177 case PATH_REF: 178 return session.getDocument(new PathRef(ref.ref)); 179 } 180 } catch (DocumentNotFoundException e) { 181 return null; 182 } 183 } catch (LocalException le) { // no such repo 184 return null; 185 } 186 } 187 } 188 return null; 189 } 190 191 @Override 192 public <T> T fetch(Class<T> type, Object value) throws IllegalStateException { 193 checkConfig(); 194 DocumentModel doc = (DocumentModel) fetch(value); 195 if (doc != null) { 196 if (type.isInstance(doc)) { 197 return type.cast(doc); 198 } 199 return doc.getAdapter(type); 200 } 201 return null; 202 } 203 204 @Override 205 public Serializable getReference(Object entity) throws IllegalStateException { 206 checkConfig(); 207 if (entity != null && entity instanceof DocumentModel) { 208 DocumentModel doc = (DocumentModel) entity; 209 String repositoryName = doc.getRepositoryName(); 210 if (repositoryName != null) { 211 switch (mode) { 212 case ID_REF: 213 return repositoryName + ":" + doc.getId(); 214 case PATH_REF: 215 return repositoryName + ":" + doc.getPath().toString(); 216 } 217 } 218 } 219 return null; 220 } 221 222 @Override 223 public String getConstraintErrorMessage(Object invalidValue, Locale locale) { 224 checkConfig(); 225 switch (mode) { 226 case ID_REF: 227 return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale); 228 case PATH_REF: 229 return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale); 230 default: 231 return String.format("%s cannot resolve reference %s", getName(), invalidValue); 232 } 233 } 234 235 private void checkConfig() throws IllegalStateException { 236 if (parameters == null) { 237 throw new IllegalStateException( 238 "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration."); 239 } 240 } 241 242 protected static final class REF { 243 244 protected String repo; 245 246 protected String ref; 247 248 protected REF() { 249 } 250 251 protected static REF fromValue(String value) { 252 String[] split = value.split(":"); 253 if (split.length == 1) { 254 REF ref = new REF(); 255 ref.repo = DEFAULT_REPO_NAME; 256 ref.ref = split[0]; 257 return ref; 258 } 259 if (split.length == 2) { 260 REF ref = new REF(); 261 ref.repo = split[0]; 262 ref.ref = split[1]; 263 return ref; 264 } 265 return null; 266 } 267 268 } 269 270}