001/* 002 * Copyright 2017 Anyware Services 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 */ 016package org.ametys.cms.search.systemprop; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.configuration.DefaultConfiguration; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.cocoon.xml.AttributesImpl; 033import org.apache.cocoon.xml.XMLUtils; 034import org.apache.commons.lang3.StringUtils; 035import org.xml.sax.ContentHandler; 036import org.xml.sax.SAXException; 037 038import org.ametys.cms.contenttype.ContentType; 039import org.ametys.cms.contenttype.ContentTypeEnumerator; 040import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 041import org.ametys.cms.contenttype.ContentTypesHelper; 042import org.ametys.cms.repository.Content; 043import org.ametys.cms.search.SearchField; 044import org.ametys.cms.search.model.SystemProperty; 045import org.ametys.cms.search.query.ContentTypeOrMixinTypeQuery; 046import org.ametys.cms.search.query.ContentTypeQuery; 047import org.ametys.cms.search.query.MixinTypeQuery; 048import org.ametys.cms.search.query.Query; 049import org.ametys.cms.search.query.Query.Operator; 050import org.ametys.cms.search.solr.field.ContentTypeSearchField; 051import org.ametys.cms.search.solr.field.MixinTypeSearchField; 052import org.ametys.core.model.type.ModelItemTypeHelper; 053import org.ametys.runtime.i18n.I18nizableText; 054import org.ametys.runtime.model.Enumerator; 055import org.ametys.runtime.model.ViewItem; 056import org.ametys.runtime.model.type.DataContext; 057import org.ametys.runtime.model.type.ModelItemTypeConstants; 058import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 059 060/** 061 * {@link SystemProperty} which represents the content types (and optionally mixins) of a content. 062 */ 063public class ContentTypeSystemProperty extends AbstractSystemProperty<String, Content> 064{ 065 /** The content type extension point. */ 066 protected ContentTypeExtensionPoint _cTypeEP; 067 068 /** The content types helper. */ 069 protected ContentTypesHelper _cTypeHelper; 070 071 /** True to include content types. */ 072 protected boolean _includeCTypes; 073 074 /** True to include mixins. */ 075 protected boolean _includeMixins; 076 077 /** True to recursively include supertypes. */ 078 protected boolean _includeSupertypes; 079 080 @Override 081 public void service(ServiceManager manager) throws ServiceException 082 { 083 super.service(manager); 084 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 085 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 086 } 087 088 @Override 089 public void configure(Configuration configuration) throws ConfigurationException 090 { 091 super.configure(configuration); 092 093 _includeCTypes = configuration.getChild("contentTypes").getValueAsBoolean(true); 094 _includeMixins = configuration.getChild("mixins").getValueAsBoolean(false); 095 _includeSupertypes = configuration.getChild("includeSupertypes").getValueAsBoolean(false); 096 } 097 098 @Override 099 public boolean isMultiple() 100 { 101 return true; 102 } 103 104 @Override 105 public boolean isSortable() 106 { 107 return true; 108 } 109 110 @Override 111 public Query getQuery(Object value, Operator operator, String language, Map<String, Object> contextualParameters) 112 { 113 String[] contentTypes = value != null ? parseStringArray(value) : new String[0]; 114 115 if (_includeCTypes && _includeMixins) 116 { 117 return new ContentTypeOrMixinTypeQuery(operator, contentTypes); 118 } 119 else if (_includeCTypes) 120 { 121 return new ContentTypeQuery(operator, contentTypes); 122 } 123 else if (_includeMixins) 124 { 125 return new MixinTypeQuery(operator, contentTypes); 126 } 127 128 return null; 129 } 130 131 @Override 132 public String getRenderer() 133 { 134 return "Ametys.plugins.cms.search.SearchGridHelper.renderMultipleString"; 135 } 136 137 @Override 138 public SearchField getSearchField() 139 { 140 if (_includeCTypes && _includeMixins) 141 { 142 return null; 143 } 144 else if (_includeCTypes) 145 { 146 return new ContentTypeSearchField(_includeSupertypes); 147 } 148 else if (_includeMixins) 149 { 150 return new MixinTypeSearchField(_includeSupertypes); 151 } 152 153 return null; 154 } 155 156 @Override 157 public Object getValue(Content content) 158 { 159 Set<String> types = new LinkedHashSet<>(); 160 161 if (_includeCTypes) 162 { 163 _addAll(types, content.getTypes()); 164 } 165 if (_includeMixins) 166 { 167 _addAll(types, content.getMixinTypes()); 168 } 169 170 return types.toArray(new String[types.size()]); 171 } 172 173 public Object valueToJSON(Content content, Optional<ViewItem> viewItem, DataContext context) 174 { 175 String[] cTypeIds = (String[]) getValue(content); 176 177 List<I18nizableText> cTypes = new ArrayList<>(); 178 for (String cTypeId : cTypeIds) 179 { 180 ContentType cType = _cTypeEP.getExtension(cTypeId); 181 if (cType != null) 182 { 183 cTypes.add(cType.getLabel()); 184 } 185 else if (_logger.isWarnEnabled()) 186 { 187 _logger.warn(String.format("Trying to get the label for an unknown content type : '%s'.", cTypeId)); 188 } 189 } 190 191 return cTypes; 192 } 193 194 @Override 195 public Object getSortValue(Content content) 196 { 197 String[] cTypeIds = (String[]) getValue(content); 198 if (cTypeIds.length > 0) 199 { 200 ContentType cType = _cTypeEP.getExtension(cTypeIds[0]); 201 if (cType != null) 202 { 203 return _i18nUtils.translate(cType.getLabel(), content.getLanguage()); 204 } 205 } 206 207 return null; 208 } 209 210 private void _addAll(Set<String> allTypes, String[] types) 211 { 212 for (String cType : types) 213 { 214 allTypes.add(cType); 215 if (_includeSupertypes && _cTypeEP.hasExtension(cType)) 216 { 217 allTypes.addAll(_cTypeHelper.getAncestors(cType)); 218 } 219 } 220 } 221 222 public void valueToSAX(ContentHandler contentHandler, Content content, Optional<ViewItem> viewItem, DataContext context) throws SAXException 223 { 224 String[] cTypeIds = (String[]) getValue(content); 225 226 XMLUtils.startElement(contentHandler, getName()); 227 228 for (String cTypeId : cTypeIds) 229 { 230 AttributesImpl attr = ModelItemTypeHelper.getXMLAttributesFromDataContext(context); 231 attr.addCDATAAttribute("id", cTypeId); 232 233 XMLUtils.startElement(contentHandler, "contentType", attr); 234 ContentType cType = _cTypeEP.getExtension(cTypeId); 235 cType.getLabel().toSAX(contentHandler); 236 XMLUtils.endElement(contentHandler, "contentType"); 237 } 238 239 XMLUtils.endElement(contentHandler, getName()); 240 } 241 242 @Override 243 public Enumerator<String> getCriterionEnumerator(Configuration configuration, ThreadSafeComponentManager<Enumerator> enumeratorManager) throws ConfigurationException 244 { 245 DefaultConfiguration conf = new DefaultConfiguration("criteria"); 246 247 DefaultConfiguration enumConf = new DefaultConfiguration("enumeration"); 248 249 DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator"); 250 customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName()); 251 252 try 253 { 254 boolean containsRefTable = false; 255 Set<String> strictContentTypes = new HashSet<>(); 256 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type")) 257 { 258 String cTypeId = cTypeConf.getAttribute("id"); 259 ContentType cType = _cTypeEP.getExtension(cTypeId); 260 if (cType == null) 261 { 262 _logger.error("A search model references a non-existing content type with id '{}', it will be ignored for system property {} ", cTypeId, getName()); 263 } 264 else 265 { 266 strictContentTypes.add(cTypeId); 267 containsRefTable = cType.isReferenceTable() || containsRefTable; 268 } 269 } 270 if (!strictContentTypes.isEmpty() && _includeCTypes) 271 { 272 DefaultConfiguration cTypeConf = new DefaultConfiguration("strictContentTypes"); 273 cTypeConf.setValue(StringUtils.join(strictContentTypes, ",")); 274 customEnumerator.addChild(cTypeConf); 275 276 if (containsRefTable) 277 { 278 // Search model focuses on at least one reference table, do not exclude reference table for content type enumerator 279 DefaultConfiguration excludeRefTableConf = new DefaultConfiguration("excludeReferenceTable"); 280 excludeRefTableConf.setValue(false); 281 customEnumerator.addChild(excludeRefTableConf); 282 } 283 } 284 } 285 catch (ConfigurationException e) 286 { 287 _logger.error("Failed to configure strict content types for system property {}", getName(), e); 288 } 289 290 if (!_includeMixins) 291 { 292 DefaultConfiguration excludeConf = new DefaultConfiguration("excludeMixin"); 293 excludeConf.setValue(true); 294 customEnumerator.addChild(excludeConf); 295 } 296 else if (!_includeCTypes) // Mixins and not CTypes 297 { 298 DefaultConfiguration includeConf = new DefaultConfiguration("includeMixinOnly"); 299 includeConf.setValue(true); 300 customEnumerator.addChild(includeConf); 301 } 302 303 DefaultConfiguration allOptionConf = new DefaultConfiguration("all-option"); 304 allOptionConf.setValue("disabled"); 305 customEnumerator.addChild(allOptionConf); 306 307 enumConf.addChild(customEnumerator); 308 conf.addChild(enumConf); 309 310 String role = "enumerator"; 311 enumeratorManager.addComponent(getPluginName(), null, role, ContentTypeEnumerator.class, conf); 312 313 try 314 { 315 enumeratorManager.initialize(); 316 return enumeratorManager.lookup(role); 317 } 318 catch (Exception e) 319 { 320 throw new ConfigurationException("Unable to initialize the content type enumerator for system property '" + getName() + "'.", conf, e); 321 } 322 } 323 324 /** 325 * Get the default widget to use when rendering this property as a criterion. 326 * @return The default widget to use. 327 */ 328 @Override 329 public String getCriterionWidget() 330 { 331 return "edition.select-content-types"; 332 } 333 334 @Override 335 public Map<String, I18nizableText> getCriterionWidgetParameters(Configuration configuration) 336 { 337 Map<String, I18nizableText> parameters = new HashMap<>(); 338 339 try 340 { 341 Set<String> strictContentTypes = new HashSet<>(); 342 for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type")) 343 { 344 strictContentTypes.add(cTypeConf.getAttribute("id")); 345 } 346 347 if (!strictContentTypes.isEmpty() && _includeCTypes) 348 { 349 parameters.put("strictContentTypes", new I18nizableText(StringUtils.join(strictContentTypes, ","))); 350 } 351 } 352 catch (ConfigurationException e) 353 { 354 _logger.error("Failed to configure strict content types for system property {}", getName(), e); 355 } 356 357 parameters.put("excludeMixin", new I18nizableText(_includeMixins ? "false" : "true")); 358 parameters.put("includeSupertype", new I18nizableText(_includeSupertypes ? "true" : "false")); 359 parameters.put("includeContentTypes", new I18nizableText(_includeCTypes ? "true" : "false")); 360 parameters.put("excludeReferenceTable", new I18nizableText("true")); 361 parameters.put("excludePrivate", new I18nizableText("true")); 362 parameters.put("emptyText", new I18nizableText("plugin.cms", "WIDGET_COMBOBOX_ALL_OPTIONS")); 363 364 return parameters; 365 } 366 367 @Override 368 protected String _getTypeId() 369 { 370 return ModelItemTypeConstants.STRING_TYPE_ID; 371 } 372}