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