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.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.cocoon.components.ContextHelper; 032import org.apache.cocoon.environment.Request; 033import org.apache.commons.lang.StringUtils; 034import org.slf4j.Logger; 035 036import org.ametys.cms.contenttype.ContentAttributeDefinition; 037import org.ametys.cms.contenttype.ContentType; 038import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 039import org.ametys.cms.contenttype.ContentTypesHelper; 040import org.ametys.cms.data.ContentValue; 041import org.ametys.cms.repository.Content; 042import org.ametys.cms.repository.ContentTypeExpression; 043import org.ametys.cms.repository.LanguageExpression; 044import org.ametys.core.util.I18nUtils; 045import org.ametys.core.util.LambdaUtils; 046import org.ametys.plugins.repository.AmetysObjectIterable; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.UnknownAmetysObjectException; 050import org.ametys.plugins.repository.query.QueryHelper; 051import org.ametys.plugins.repository.query.SortCriteria; 052import org.ametys.plugins.repository.query.expression.AndExpression; 053import org.ametys.plugins.repository.query.expression.Expression; 054import org.ametys.plugins.repository.query.expression.Expression.Operator; 055import org.ametys.plugins.repository.query.expression.MetadataExpression; 056import org.ametys.plugins.repository.query.expression.NotExpression; 057import org.ametys.plugins.repository.query.expression.OrExpression; 058import org.ametys.plugins.repository.query.expression.StringExpression; 059import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression; 060import org.ametys.runtime.model.ElementDefinition; 061import org.ametys.runtime.model.Enumerator; 062import org.ametys.runtime.model.ModelItem; 063import org.ametys.runtime.plugin.component.AbstractLogEnabled; 064import org.ametys.web.WebConstants; 065import org.ametys.web.WebHelper; 066import org.ametys.web.repository.SiteAwareAmetysObject; 067import org.ametys.web.repository.page.Page; 068import org.ametys.web.repository.page.PageQueryHelper; 069 070/** 071 * Component providing methods to retrieve ugc virtual pages, such as the ugc root, 072 * transitional page and ugc content page. 073 */ 074public class UGCPageHandler extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 075{ 076 /** The attribute to get the name of transitional page */ 077 public static final String ATTRIBUTE_TRANSITIONAL_PAGE_METADATA_VALUE = "metadata_value"; 078 079 /** The attribute to get the title of transitional page */ 080 public static final String ATTRIBUTE_TRANSITIONAL_PAGE_TITLE = "title"; 081 082 /** The avalon role. */ 083 public static final String ROLE = UGCPageHandler.class.getName(); 084 085 /** The data name for the content type of the ugc */ 086 public static final String CONTENT_TYPE_DATA_NAME = "ugc-root-contenttype"; 087 088 /** The data name for the classification attribute of the ugc */ 089 public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "ugc-root-classification-metadata"; 090 091 /** The data name for the visibility of transitional page of the ugc */ 092 public static final String CLASSIFICATION_PAGE_VISIBLE_DATA_NAME = "ugc-root-classification-page-visible"; 093 094 /** The ametys object resolver */ 095 protected AmetysObjectResolver _resolver; 096 097 /** The content type extension point */ 098 protected ContentTypeExtensionPoint _cTypeEP; 099 100 /** The content types helper */ 101 protected ContentTypesHelper _cTypeHelper; 102 103 /** The i18n utils */ 104 protected I18nUtils _i18nUtils; 105 106 /** The avalon context */ 107 protected Context _context; 108 109 110 @Override 111 public void service(ServiceManager manager) throws ServiceException 112 { 113 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 114 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 115 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 116 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 117 } 118 119 public void contextualize(Context context) throws ContextException 120 { 121 _context = context; 122 } 123 124 @Override 125 protected Logger getLogger() 126 { 127 return super.getLogger(); 128 } 129 130 /** 131 * Gets the path of the classification attribute 132 * @param rootPage The ugc root page 133 * @return the path of the classification attribute 134 */ 135 public String getClassificationAttribute(Page rootPage) 136 { 137 return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME); 138 } 139 140 /** 141 * Gets the content type id 142 * @param rootPage The ugc root page 143 * @return the content type id 144 */ 145 public String getContentTypeId(Page rootPage) 146 { 147 return rootPage.getValue(CONTENT_TYPE_DATA_NAME); 148 } 149 150 /** 151 * <code>true</code> if the classification pages are visible 152 * @param rootPage The ugc root page 153 * @return <code>true</code> if the classification pages are visible 154 */ 155 public boolean isClassificationPagesVisible(Page rootPage) 156 { 157 return rootPage.getValue(CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, false); 158 } 159 160 /** 161 * Gets the ugc root pages from the given content type id. 162 * @param siteName the site name 163 * @param sitemapName the sitemap name 164 * @param contentTypeId The content type id 165 * @return the ugc root page. 166 * @throws AmetysRepositoryException if an error occured. 167 */ 168 public Page getUGCRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException 169 { 170 Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName()); 171 Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId); 172 173 AndExpression andExp = new AndExpression(expression, contentTypeExp); 174 175 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, andExp, null); 176 177 AmetysObjectIterable<Page> pages = _resolver.query(query); 178 179 return pages.iterator().hasNext() ? pages.iterator().next() : null; 180 } 181 182 /** 183 * Get the ugc root pages 184 * @param siteName the current site. 185 * @param sitemapName the sitemap name. 186 * @return the ugc root pages 187 * @throws AmetysRepositoryException if an error occured. 188 */ 189 public Set<Page> getUGCRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 190 { 191 Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName()); 192 193 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 194 195 AmetysObjectIterable<Page> pages = _resolver.query(query); 196 197 return pages.stream().collect(Collectors.toSet()); 198 } 199 200 /** 201 * Get UGC contents from rootPage 202 * @param rootPage the root page 203 * @return the list of UGC contents 204 */ 205 public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage) 206 { 207 String lang = rootPage.getSitemapName(); 208 String contentType = getContentTypeId(rootPage); 209 210 ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType); 211 212 StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName()); 213 Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE)); 214 Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr); 215 216 Expression finalExpr = new AndExpression(contentTypeExp, new LanguageExpression(Operator.EQ, lang), fullSiteExpr); 217 218 SortCriteria sort = new SortCriteria(); 219 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 220 221 String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort); 222 223 return _resolver.query(xPathQuery); 224 } 225 226 /** 227 * Determines if content is part of UGC root page 228 * @param rootPage The root page 229 * @param content the content 230 * @return true if content is part of UGC root page 231 */ 232 public boolean hasContentForRootPage(Page rootPage, Content content) 233 { 234 String contentType = getContentTypeId(rootPage); 235 String siteName = rootPage.getSiteName(); 236 String classificationMetadata = getClassificationAttribute(rootPage); 237 238 return _cTypeHelper.isInstanceOf(content, contentType) // match content type 239 && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site 240 && StringUtils.isBlank(classificationMetadata); // no classification attribute 241 } 242 243 /** 244 * Get the map of transitional page (name : (id, title)) 245 * @param rootPage the root page 246 * @return The map of transitional page 247 */ 248 public Map<String, Map<String, String>> getTransitionalPage(Page rootPage) 249 { 250 return _getClassificationType(rootPage) 251 .allTransitionalPages() 252 .stream() 253 .sorted(Comparator.comparing(TransitionalPageInformation::getTitle)) 254 .collect(LambdaUtils.Collectors.toLinkedHashMap( 255 TransitionalPageInformation::getKey, 256 TransitionalPageInformation::getInfo)); 257 } 258 259 private ClassificationType _getClassificationType(Page rootPage) 260 { 261 String classificationAttributePath = getClassificationAttribute(rootPage); 262 if (StringUtils.isBlank(classificationAttributePath)) 263 { 264 // No classification attribute defined, so no transitional page 265 return new ClassificationType.None(); 266 } 267 String contentTypeId = getContentTypeId(rootPage); 268 ContentType contentType = _cTypeEP.getExtension(contentTypeId); 269 270 if (contentType == null) 271 { 272 getLogger().warn("Can not classify UGC content of type '" + contentTypeId + "' on root page " + rootPage.getId()); 273 } 274 else if (contentType.hasModelItem(classificationAttributePath)) 275 { 276 ModelItem modelItem = contentType.getModelItem(classificationAttributePath); 277 if (modelItem instanceof ContentAttributeDefinition) 278 { 279 String attributeContentType = ((ContentAttributeDefinition) modelItem).getContentTypeId(); 280 return new ClassificationType.TypeContent(this, rootPage, attributeContentType); 281 } 282 else if (modelItem instanceof ElementDefinition<?>) 283 { 284 @SuppressWarnings("unchecked") 285 Enumerator<String> enumerator = ((ElementDefinition<String>) modelItem).getEnumerator(); 286 if (enumerator != null) 287 { 288 return new ClassificationType.TypeEnum(this, rootPage, enumerator); 289 } 290 } 291 } 292 293 return new ClassificationType.None(); 294 } 295 296 /** 297 * Get contents under transitional page 298 * @param rootPage the root page 299 * @param metadataValue the metadata value (linked to the transitional page) 300 * @return list of contents under transitional page 301 */ 302 public AmetysObjectIterable<Content> getContentsForTransitionalPage(Page rootPage, String metadataValue) 303 { 304 String classificationMetadata = getClassificationAttribute(rootPage); 305 306 String lang = rootPage.getSitemapName(); 307 String contentType = getContentTypeId(rootPage); 308 309 ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType); 310 StringExpression metadataExpression = new StringExpression(classificationMetadata, Operator.EQ, metadataValue); 311 312 StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName()); 313 Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE)); 314 Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr); 315 316 Expression finalExpr = new AndExpression(contentTypeExp, metadataExpression, new LanguageExpression(Operator.EQ, lang), fullSiteExpr); 317 318 SortCriteria sort = new SortCriteria(); 319 sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true); 320 321 String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort); 322 323 return _resolver.query(xPathQuery); 324 } 325 326 /** 327 * Determines if given content is part of a transitional page 328 * @param rootPage the root page 329 * @param metadataValue the metadata value (linked to the transitional page). Cannot be null. 330 * @param content the content 331 * @return true if content is part of the transitional page 332 */ 333 public boolean hasContentForTransitionalPage(Page rootPage, String metadataValue, Content content) 334 { 335 String contentType = getContentTypeId(rootPage); 336 String siteName = rootPage.getSiteName(); 337 String classificationMetadata = getClassificationAttribute(rootPage); 338 339 return _cTypeHelper.isInstanceOf(content, contentType) // match content type 340 && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site 341 && StringUtils.isNotEmpty(classificationMetadata) && metadataValue.equals(((ContentValue) content.getValue(classificationMetadata)).getContentId()); // match classification attribute 342 343 } 344 345 /** 346 * Computes a page id 347 * @param path The path 348 * @param root The root page 349 * @param ugcContent The UGC content 350 * @return The id 351 */ 352 public String computePageId(String path, Page root, Content ugcContent) 353 { 354 // E.g: ugccontent://path?rootId=...&contentId=... 355 return "ugccontent://" + path + "?rootId=" + root.getId() + "&contentId=" + ugcContent.getId(); 356 } 357 358 /** 359 * Gets the UGC page related to the given UG Content id 360 * @param contentId the id of UG Content 361 * @param siteName The site name. Can be nul to get site from content or current site. 362 * @return the UGC page or null if not found 363 */ 364 public UGCPage getUgcPage(String contentId, String siteName) 365 { 366 if (contentId == null) 367 { 368 return null; 369 } 370 371 Content content; 372 try 373 { 374 content = _resolver.resolveById(contentId); 375 } 376 catch (UnknownAmetysObjectException e) 377 { 378 return null; 379 } 380 381 Request request = ContextHelper.getRequest(_context); 382 String site = StringUtils.isNotBlank(siteName) ? siteName : WebHelper.getSiteName(request, content); 383 384 String sitemap = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME); 385 386 for (String type : content.getTypes()) 387 { 388 Optional<UGCPage> ugcPage = getUgcPage(content, site, sitemap, type); 389 if (ugcPage.isPresent()) 390 { 391 return ugcPage.get(); 392 } 393 } 394 395 return null; 396 } 397 398 /** 399 * Gets the UGC page related to the given UG Content for given site, sitemap and type 400 * @param ugcContent the UG Content 401 * @param site the site name 402 * @param sitemap the sitemap name 403 * @param contentType the content type id 404 * @return the UGC page 405 */ 406 public Optional<UGCPage> getUgcPage(Content ugcContent, String site, String sitemap, String contentType) 407 { 408 String language = Optional.of(ugcContent) 409 .map(Content::getLanguage) 410 .orElse(sitemap); 411 Page ugcRootPage = getUGCRootPage(site, language, contentType); 412 413 return Optional.ofNullable(ugcRootPage) 414 .flatMap(root -> getUgcPage(root, ugcContent)); 415 } 416 417 /** 418 * Gets the UGC page related to the given UG Content for given UGC root 419 * @param ugcRootPage the UGC root page 420 * @param ugcContent the UG Content 421 * @return the UGC page 422 */ 423 public Optional<UGCPage> getUgcPage(Page ugcRootPage, Content ugcContent) 424 { 425 String path = _getPath(ugcRootPage, ugcContent); 426 return Optional.ofNullable(path) 427 .map(p -> computePageId(p, ugcRootPage, ugcContent)) 428 .map(this::_silentResolve); 429 } 430 431 private String _getPath(Page ugcRootPage, Content ugcContent) 432 { 433 try 434 { 435 ClassificationType transtionalPageType = _getClassificationType(ugcRootPage); 436 if (transtionalPageType instanceof ClassificationType.None) 437 { 438 return "_root"; 439 } 440 else 441 { 442 TransitionalPageInformation transitionalPageInfo = transtionalPageType.getTransitionalPage(ugcContent); 443 return transitionalPageInfo.getKey(); 444 } 445 } 446 catch (Exception e) 447 { 448 getLogger().error("Cannot get path for root {} and content {}", ugcRootPage, ugcContent, e); 449 return null; 450 } 451 } 452 453 private UGCPage _silentResolve(String id) 454 { 455 try 456 { 457 return _resolver.resolveById(id); 458 } 459 catch (UnknownAmetysObjectException e) 460 { 461 return null; 462 } 463 } 464}