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