001/* 002 * (C) Copyright 2014-2018 Nuxeo (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 <[email protected]> 018 */ 019 020package org.nuxeo.ecm.directory; 021 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027 028import org.apache.commons.lang3.StringUtils; 029import org.nuxeo.ecm.core.api.DocumentModel; 030import org.nuxeo.ecm.core.schema.types.resolver.AbstractObjectResolver; 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 extends AbstractObjectResolver 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 boolean hierarchical = false; 090 091 private String parentField = null; 092 093 private String separator = null; 094 095 private List<Class<?>> managedClasses = null; 096 097 private String directoryName; 098 099 @Override 100 public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException { 101 super.configure(parameters); 102 directoryName = parameters.get(PARAM_DIRECTORY); 103 if (directoryName != null) { 104 directoryName = directoryName.trim(); 105 } 106 if (directoryName == null || directoryName.isEmpty()) { 107 throw new IllegalArgumentException("missing directory parameter. A directory name is necessary"); 108 } 109 Directory directory = getDirectory(); 110 idField = directory.getIdField(); 111 schema = directory.getSchema(); 112 if (schema.endsWith("xvocabulary")) { 113 hierarchical = true; 114 parentField = "parent"; 115 separator = "/"; 116 } 117 String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD)); 118 String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR)); 119 if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) { 120 hierarchical = true; 121 parentField = parentFieldParam; 122 separator = separatorParam; 123 } 124 this.parameters.put(PARAM_DIRECTORY, directoryName); 125 } 126 127 @Override 128 public List<Class<?>> getManagedClasses() { 129 if (managedClasses == null) { 130 managedClasses = new ArrayList<>(); 131 managedClasses.add(DirectoryEntry.class); 132 } 133 return managedClasses; 134 } 135 136 public Directory getDirectory() { 137 DirectoryService directoryService = Framework.getService(DirectoryService.class); 138 Directory directory = directoryService.getDirectory(directoryName); 139 if (directory == null) { 140 throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName)); 141 } 142 return directory; 143 } 144 145 @Override 146 public String getName() { 147 checkConfig(); 148 return NAME; 149 } 150 151 @Override 152 public Object fetch(Object value) throws IllegalStateException { 153 checkConfig(); 154 if (value instanceof String) { 155 String id = (String) value; 156 if (hierarchical) { 157 String[] ids = StringUtils.split(id, separator); 158 if (ids.length > 0) { 159 id = ids[ids.length - 1]; 160 } else { 161 return null; 162 } 163 } 164 try (Session session = getDirectory().getSession()) { 165 String finalId = id; // Effectively final 166 DocumentModel doc = Framework.doPrivileged(() -> session.getEntry(finalId)); 167 if (doc != null) { 168 return new DirectoryEntry(directoryName, doc); 169 } 170 return null; 171 } 172 } 173 return null; 174 } 175 176 @Override 177 public <T> T fetch(Class<T> type, Object value) throws IllegalStateException { 178 checkConfig(); 179 DirectoryEntry doc = (DirectoryEntry) fetch(value); 180 if (doc != null) { 181 if (type.isInstance(doc)) { 182 return type.cast(doc); 183 } 184 if (type.isInstance(doc.getDocumentModel())) { 185 return type.cast(doc.getDocumentModel()); 186 } 187 } 188 return null; 189 } 190 191 @Override 192 public Serializable getReference(Object entity) throws IllegalStateException { 193 checkConfig(); 194 DocumentModel entry = null; 195 if (entity != null) { 196 if (entity instanceof DirectoryEntry) { 197 entry = ((DirectoryEntry) entity).getDocumentModel(); 198 } else if (entity instanceof DocumentModel) { 199 entry = (DocumentModel) entity; 200 } 201 if (entry != null) { 202 if (!entry.hasSchema(schema)) { 203 return null; 204 } 205 String result = (String) entry.getProperty(schema, idField); 206 if (hierarchical) { 207 String parent = (String) entry.getProperty(schema, parentField); 208 try (Session session = getDirectory().getSession()) { 209 while (parent != null) { 210 String finalParent = parent; // Effectively final 211 entry = Framework.doPrivileged(() -> session.getEntry(finalParent)); 212 if (entry == null) { 213 break; 214 } 215 result = parent + separator + result; 216 parent = (String) entry.getProperty(schema, parentField); 217 } 218 } 219 } 220 return result; 221 } 222 } 223 return null; 224 } 225 226 @Override 227 public String getConstraintErrorMessage(Object invalidValue, Locale locale) { 228 checkConfig(); 229 return Helper.getConstraintErrorMessage(this, invalidValue, locale, directoryName); 230 } 231 232}