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