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