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.directory; 019 020import java.io.IOException; 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028 029import org.apache.commons.lang.StringUtils; 030import org.nuxeo.ecm.core.api.DocumentModel; 031import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 032import org.nuxeo.ecm.directory.api.DirectoryEntry; 033import org.nuxeo.ecm.directory.api.DirectoryService; 034import org.nuxeo.runtime.api.Framework; 035 036/** 037 * This {@link ObjectResolver} allows to manage integrity for fields containing references to directory's entry. 038 * <p> 039 * References contains the directory entry id. 040 * </p> 041 * <p> 042 * To use it, put the following code in your schema XSD (don't forget the directory name): 043 * </p> 044 * 045 * <pre> 046 * {@code 047 * <xs:element name="carBrand"> 048 * <xs:simpleType> 049 * <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="carBrandsDirectory" /> 050 * </xs:simpleType> 051 * </xs:element> 052 * </pre> 053 * <p> 054 * For hierarchical directories, which entries reference other entries. You can manage a specific reference containing 055 * the full entry path. You have to specify the parent field and the separator used to encode the reference. 056 * </p> 057 * 058 * <pre> 059 * {@code 060 * <xs:element name="coverage"> 061 * <xs:simpleType> 062 * <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="l10ncoverage" ref:parentField="parent" ref:separator="/" /> 063 * </xs:simpleType> 064 * </xs:element> 065 * </pre> 066 * <p> 067 * It's not necessary to define parentField and separator for directory using schema ending by xvocabulary. The feature 068 * is automatically enable. 069 * </p> 070 * 071 * @since 7.1 072 */ 073public class DirectoryEntryResolver implements ObjectResolver { 074 075 private static final long serialVersionUID = 1L; 076 077 public static final String NAME = "directoryResolver"; 078 079 public static final String PARAM_DIRECTORY = "directory"; 080 081 public static final String PARAM_PARENT_FIELD = "parentField"; 082 083 public static final String PARAM_SEPARATOR = "separator"; 084 085 private String idField; 086 087 private String schema; 088 089 private Map<String, Serializable> parameters; 090 091 private boolean hierarchical = false; 092 093 private String parentField = null; 094 095 private String separator = null; 096 097 private List<Class<?>> managedClasses = null; 098 099 private String directoryName; 100 101 /** 102 * the directory is transient - it's refetched on read object - see {@link #readObject(java.io.ObjectInputStream)} 103 */ 104 private transient Directory directory; 105 106 private transient DirectoryService directoryService; 107 108 @Override 109 public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException { 110 if (this.parameters != null) { 111 throw new IllegalStateException("cannot change configuration, may be already in use somewhere"); 112 } 113 directoryName = parameters.get(PARAM_DIRECTORY); 114 if (directoryName != null) { 115 directoryName = directoryName.trim(); 116 } 117 if (directoryName == null || directoryName.isEmpty()) { 118 throw new IllegalArgumentException("missing directory parameter. A directory name is necessary"); 119 } 120 fetchDirectory(); 121 idField = directory.getIdField(); 122 schema = directory.getSchema(); 123 if (schema.endsWith("xvocabulary")) { 124 hierarchical = true; 125 parentField = "parent"; 126 separator = "/"; 127 } 128 String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD)); 129 String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR)); 130 if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) { 131 hierarchical = true; 132 parentField = parentFieldParam; 133 separator = separatorParam; 134 } 135 this.parameters = new HashMap<String, Serializable>(); 136 this.parameters.put(PARAM_DIRECTORY, directory.getName()); 137 } 138 139 @Override 140 public List<Class<?>> getManagedClasses() { 141 if (managedClasses == null) { 142 managedClasses = new ArrayList<Class<?>>(); 143 managedClasses.add(DirectoryEntry.class); 144 } 145 return managedClasses; 146 } 147 148 private void fetchDirectory() { 149 directory = getDirectoryService().getDirectory(directoryName); 150 if (directory == null) { 151 throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName)); 152 } 153 } 154 155 public DirectoryService getDirectoryService() { 156 if (directoryService == null) { 157 directoryService = Framework.getService(DirectoryService.class); 158 } 159 return directoryService; 160 } 161 162 public Directory getDirectory() { 163 return directory; 164 } 165 166 public void setDirectory(Directory directory) { 167 this.directory = directory; 168 } 169 170 @Override 171 public String getName() { 172 checkConfig(); 173 return NAME; 174 } 175 176 @Override 177 public Map<String, Serializable> getParameters() { 178 checkConfig(); 179 return Collections.unmodifiableMap(parameters); 180 } 181 182 @Override 183 public boolean validate(Object value) throws IllegalStateException { 184 checkConfig(); 185 return fetch(value) != null; 186 } 187 188 @Override 189 public Object fetch(Object value) throws IllegalStateException { 190 checkConfig(); 191 if (value != null && value instanceof String) { 192 String id = (String) value; 193 if (hierarchical) { 194 String[] ids = StringUtils.split(id, separator); 195 if (ids.length > 0) { 196 id = ids[ids.length - 1]; 197 } else { 198 return null; 199 } 200 } 201 try (Session session = directory.getSession()) { 202 DocumentModel doc = session.getEntry(id); 203 if (doc != null) { 204 return new DirectoryEntry(directory.getName(), doc); 205 } 206 return null; 207 } 208 } 209 return null; 210 } 211 212 @Override 213 public <T> T fetch(Class<T> type, Object value) throws IllegalStateException { 214 checkConfig(); 215 DirectoryEntry doc = (DirectoryEntry) fetch(value); 216 if (doc != null) { 217 if (type.isInstance(doc)) { 218 return type.cast(doc); 219 } 220 if (type.isInstance(doc.getDocumentModel())) { 221 return type.cast(doc.getDocumentModel()); 222 } 223 } 224 return null; 225 } 226 227 @Override 228 public Serializable getReference(Object entity) throws IllegalStateException { 229 checkConfig(); 230 DocumentModel entry = null; 231 if (entity != null) { 232 if (entity instanceof DirectoryEntry) { 233 entry = ((DirectoryEntry) entity).getDocumentModel(); 234 } else if (entity instanceof DocumentModel) { 235 entry = (DocumentModel) entity; 236 } 237 if (entry != null) { 238 if (!entry.hasSchema(schema)) { 239 return null; 240 } 241 String result = (String) entry.getProperty(schema, idField); 242 if (hierarchical) { 243 String parent = (String) entry.getProperty(schema, parentField); 244 try (Session session = directory.getSession()) { 245 while (parent != null) { 246 entry = session.getEntry(parent); 247 if (entry == null) { 248 break; 249 } 250 result = parent + separator + result; 251 parent = (String) entry.getProperty(schema, parentField); 252 } 253 } 254 } 255 return result; 256 } 257 } 258 return null; 259 } 260 261 @Override 262 public String getConstraintErrorMessage(Object invalidValue, Locale locale) { 263 checkConfig(); 264 return Helper.getConstraintErrorMessage(this, invalidValue, locale, directory.getName()); 265 } 266 267 private void checkConfig() throws IllegalStateException { 268 if (parameters == null) { 269 throw new IllegalStateException( 270 "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration."); 271 } 272 } 273 274 /** 275 * Refetch the directory which is transient. 276 * 277 * @since 7.10 278 */ 279 private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { 280 stream.defaultReadObject(); 281 fetchDirectory(); 282 } 283 284}