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