001/* 002 * Copyright 2018 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.plugins.ugc.page; 017 018import java.util.Comparator; 019import java.util.Map; 020import java.util.Optional; 021import java.util.Set; 022import java.util.stream.Collectors; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang.StringUtils; 029import org.slf4j.Logger; 030 031import org.ametys.cms.contenttype.ContentAttributeDefinition; 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 034import org.ametys.cms.repository.Content; 035import org.ametys.cms.repository.ContentTypeExpression; 036import org.ametys.cms.repository.LanguageExpression; 037import org.ametys.core.util.I18nUtils; 038import org.ametys.core.util.LambdaUtils; 039import org.ametys.plugins.repository.AmetysObjectIterable; 040import org.ametys.plugins.repository.AmetysObjectResolver; 041import org.ametys.plugins.repository.AmetysRepositoryException; 042import org.ametys.plugins.repository.UnknownAmetysObjectException; 043import org.ametys.plugins.repository.query.QueryHelper; 044import org.ametys.plugins.repository.query.SortCriteria; 045import org.ametys.plugins.repository.query.expression.AndExpression; 046import org.ametys.plugins.repository.query.expression.Expression; 047import org.ametys.plugins.repository.query.expression.Expression.Operator; 048import org.ametys.plugins.repository.query.expression.StringExpression; 049import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression; 050import org.ametys.runtime.model.ElementDefinition; 051import org.ametys.runtime.model.Enumerator; 052import org.ametys.runtime.model.ModelItem; 053import org.ametys.runtime.plugin.component.AbstractLogEnabled; 054import org.ametys.web.repository.page.Page; 055import org.ametys.web.repository.page.PageQueryHelper; 056 057/** 058 * Component providing methods to retrieve ugc virtual pages, such as the ugc root, 059 * transitional page and ugc content page. 060 */ 061public class UGCPageHandler extends AbstractLogEnabled implements Component, Serviceable 062{ 063 /** The attribute to get the name of transitional page */ 064 public static final String ATTRIBUTE_TRANSITIONAL_PAGE_METADATA_VALUE = "metadata_value"; 065 066 /** The attribute to get the title of transitional page */ 067 public static final String ATTRIBUTE_TRANSITIONAL_PAGE_TITLE = "title"; 068 069 /** The avalon role. */ 070 public static final String ROLE = UGCPageHandler.class.getName(); 071 072 /** The data name for the content type of the ugc */ 073 public static final String CONTENT_TYPE_DATA_NAME = "ugc-root-contenttype"; 074 075 /** The data name for the classification attribute of the ugc */ 076 public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "ugc-root-classification-metadata"; 077 078 /** The data name for the visibility of transitional page of the ugc */ 079 public static final String CLASSIFICATION_PAGE_VISIBLE_DATA_NAME = "ugc-root-classification-page-visible"; 080 081 /** The ametys object resolver */ 082 protected AmetysObjectResolver _resolver; 083 084 /** The content type extension point */ 085 protected ContentTypeExtensionPoint _cTypeEP; 086 087 /** The i18n utils */ 088 protected I18nUtils _i18nUtils; 089 090 @Override 091 public void service(ServiceManager manager) throws ServiceException 092 { 093 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 094 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 095 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 096 } 097 098 @Override 099 protected Logger getLogger() 100 { 101 return super.getLogger(); 102 } 103 104 /** 105 * Gets the path of the classification attribute 106 * @param rootPage The ugc root page 107 * @return the path of the classification attribute 108 */ 109 public String getClassificationAttribute(Page rootPage) 110 { 111 return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME); 112 } 113 114 /** 115 * Gets the content type id 116 * @param rootPage The ugc root page 117 * @return the content type id 118 */ 119 public String getContentTypeId(Page rootPage) 120 { 121 return rootPage.getValue(CONTENT_TYPE_DATA_NAME); 122 } 123 124 /** 125 * <code>true</code> if the classification pages are visible 126 * @param rootPage The ugc root page 127 * @return <code>true</code> if the classification pages are visible 128 */ 129 public boolean isClassificationPagesVisible(Page rootPage) 130 { 131 return rootPage.getValue(CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, false); 132 } 133 134 /** 135 * Gets the ugc root pages from the given content type id. 136 * @param siteName the site name 137 * @param sitemapName the sitemap name 138 * @param contentTypeId The content type id 139 * @return the ugc root page. 140 * @throws AmetysRepositoryException if an error occured. 141 */ 142 public Page getUGCRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException 143 { 144 Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName()); 145 Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId); 146 147 AndExpression andExp = new AndExpression(expression, contentTypeExp); 148 149 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, andExp, null); 150 151 AmetysObjectIterable<Page> pages = _resolver.query(query); 152 153 return pages.iterator().hasNext() ? pages.iterator().next() : null; 154 } 155 156 /** 157 * Get the ugc root pages 158 * @param siteName the current site. 159 * @param sitemapName the sitemap name. 160 * @return the ugc root pages 161 * @throws AmetysRepositoryException if an error occured. 162 */ 163 public Set<Page> getUGCRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 164 { 165 Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName()); 166 167 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 168 169 AmetysObjectIterable<Page> pages = _resolver.query(query); 170 171 return pages.stream().collect(Collectors.toSet()); 172 } 173 174 /** 175 * Get orgUnit contents from rootPage 176 * @param rootPage the root page 177 * @return the list of orgUnit contents 178 */ 179 public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage) 180 { 181 String lang = rootPage.getSitemapName(); 182 String contentType = getContentTypeId(rootPage); 183 184 ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType); 185 186 Expression finalExpr = new AndExpression(contentTypeExp, new LanguageExpression(Operator.EQ, lang)); 187 188 SortCriteria sort = new SortCriteria(); 189 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 190 191 String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort); 192 193 return _resolver.query(xPathQuery); 194 } 195 196 /** 197 * Get the map of transitional page (name : (id, title)) 198 * @param rootPage the root page 199 * @return The map of transitional page 200 */ 201 public Map<String, Map<String, String>> getTransitionalPage(Page rootPage) 202 { 203 return _getClassificationType(rootPage) 204 .allTransitionalPages() 205 .stream() 206 .sorted(Comparator.comparing(TransitionalPageInformation::getTitle)) 207 .collect(LambdaUtils.Collectors.toLinkedHashMap( 208 TransitionalPageInformation::getKey, 209 TransitionalPageInformation::getInfo)); 210 } 211 212 private ClassificationType _getClassificationType(Page rootPage) 213 { 214 String classificationAttributePath = getClassificationAttribute(rootPage); 215 if (StringUtils.isBlank(classificationAttributePath)) 216 { 217 // No classification attribute defined, so no transitional page 218 return new ClassificationType.None(); 219 } 220 String contentTypeId = getContentTypeId(rootPage); 221 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 222 223 if (contentType == null) 224 { 225 getLogger().warn("Can not classify UGC content of type '" + contentTypeId + "' on root page " + rootPage.getId()); 226 } 227 else if (contentType.hasModelItem(classificationAttributePath)) 228 { 229 ModelItem modelItem = contentType.getModelItem(classificationAttributePath); 230 if (modelItem instanceof ContentAttributeDefinition) 231 { 232 String attributeContentType = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 233 return new ClassificationType.TypeContent(this, rootPage, attributeContentType); 234 } 235 else if (modelItem instanceof ElementDefinition<?>) 236 { 237 @SuppressWarnings("unchecked") 238 Enumerator<String> enumerator = ((ElementDefinition<String>) modelItem).getEnumerator(); 239 if (enumerator != null) 240 { 241 return new ClassificationType.TypeEnum(this, rootPage, enumerator); 242 } 243 } 244 } 245 246 return new ClassificationType.None(); 247 } 248 249 /** 250 * Get contents under transitional page 251 * @param rootPage the root page 252 * @param metadataValue the metadata value (linked to the transitional page) 253 * @return list of contents under transitional page 254 */ 255 public AmetysObjectIterable<Content> getContentsForTransitionalPage(Page rootPage, String metadataValue) 256 { 257 String classificationMetadata = getClassificationAttribute(rootPage); 258 259 String lang = rootPage.getSitemapName(); 260 String contentType = getContentTypeId(rootPage); 261 262 ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType); 263 StringExpression metadataExpression = new StringExpression(classificationMetadata, Operator.EQ, metadataValue); 264 265 Expression finalExpr = new AndExpression(contentTypeExp, metadataExpression, new LanguageExpression(Operator.EQ, lang)); 266 267 SortCriteria sort = new SortCriteria(); 268 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 269 270 String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort); 271 272 return _resolver.query(xPathQuery); 273 } 274 275 /** 276 * Computes a page id 277 * @param path The path 278 * @param root The root page 279 * @param ugcContent The UGC content 280 * @return The id 281 */ 282 public String computePageId(String path, Page root, Content ugcContent) 283 { 284 // E.g: ugccontent://path?rootId=...&contentId=... 285 return "ugccontent://" + path + "?rootId=" + root.getId() + "&contentId=" + ugcContent.getId(); 286 } 287 288 /** 289 * Gets the UGC page related to the given UG Content for given site, sitemap and type 290 * @param ugcContent the UG Content 291 * @param site the site name 292 * @param sitemap the sitemap name 293 * @param contentType the content type id 294 * @return the UGC page 295 */ 296 public Optional<UGCPage> getUgcPage(Content ugcContent, String site, String sitemap, String contentType) 297 { 298 String language = Optional.of(ugcContent) 299 .map(Content::getLanguage) 300 .orElse(sitemap); 301 Page ugcRootPage = getUGCRootPage(site, language, contentType); 302 303 return Optional.ofNullable(ugcRootPage) 304 .flatMap(root -> getUgcPage(root, ugcContent)); 305 } 306 307 /** 308 * Gets the UGC page related to the given UG Content for given UGC root 309 * @param ugcRootPage the UGC root page 310 * @param ugcContent the UG Content 311 * @return the UGC page 312 */ 313 public Optional<UGCPage> getUgcPage(Page ugcRootPage, Content ugcContent) 314 { 315 String path = _getPath(ugcRootPage, ugcContent); 316 return Optional.ofNullable(path) 317 .map(p -> computePageId(p, ugcRootPage, ugcContent)) 318 .map(this::_silentResolve); 319 } 320 321 private String _getPath(Page ugcRootPage, Content ugcContent) 322 { 323 try 324 { 325 ClassificationType transtionalPageType = _getClassificationType(ugcRootPage); 326 if (transtionalPageType instanceof ClassificationType.None) 327 { 328 return "_root"; 329 } 330 else 331 { 332 TransitionalPageInformation transitionalPageInfo = transtionalPageType.getTransitionalPage(ugcContent); 333 return transitionalPageInfo.getKey(); 334 } 335 } 336 catch (Exception e) 337 { 338 getLogger().error("Cannot get path for root {} and content {}", ugcRootPage, ugcContent, e); 339 return null; 340 } 341 } 342 343 private UGCPage _silentResolve(String id) 344 { 345 try 346 { 347 return _resolver.resolveById(id); 348 } 349 catch (UnknownAmetysObjectException e) 350 { 351 return null; 352 } 353 } 354}