001/* 002 * Copyright 2014 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.tag.jcr; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027 028import javax.xml.parsers.SAXParserFactory; 029 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 033import org.apache.avalon.framework.parameters.Parameters; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.cocoon.environment.ObjectModelHelper; 037import org.apache.cocoon.environment.Redirector; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.environment.SourceResolver; 040import org.apache.cocoon.servlet.multipart.Part; 041import org.apache.cocoon.servlet.multipart.PartOnDisk; 042import org.apache.cocoon.servlet.multipart.RejectedPart; 043import org.apache.commons.io.FilenameUtils; 044import org.apache.commons.lang3.StringUtils; 045import org.xml.sax.XMLReader; 046 047import org.ametys.cms.ObservationConstants; 048import org.ametys.cms.tag.CMSTag; 049import org.ametys.cms.tag.CMSTag.TagVisibility; 050import org.ametys.cms.tag.Tag; 051import org.ametys.cms.tag.TagProvider; 052import org.ametys.cms.tag.TagProviderExtensionPoint; 053import org.ametys.cms.tag.TagTargetType; 054import org.ametys.cms.tag.TagTargetTypeExtensionPoint; 055import org.ametys.core.cocoon.JSonReader; 056import org.ametys.core.observation.AbstractNotifierAction; 057import org.ametys.core.observation.Event; 058import org.ametys.core.util.JSONUtils; 059import org.ametys.plugins.repository.AmetysObjectResolver; 060import org.ametys.plugins.repository.TraversableAmetysObject; 061import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 062import org.ametys.runtime.i18n.I18nizableText; 063 064/** 065 * Import subscribers from a CSV or text file. 066 */ 067public class ImportTagsAction extends AbstractNotifierAction 068{ 069 private static final String[] _ALLOWED_EXTENSIONS = new String[] {"xml"}; 070 071 /** The tag provider extension point */ 072 protected TagProviderExtensionPoint _tagProviderExtPt; 073 /** The Ametys object resolver */ 074 protected AmetysObjectResolver _resolver; 075 /** Target types */ 076 protected TagTargetTypeExtensionPoint _targetTypeEP; 077 /** The JSon Utils Component */ 078 protected JSONUtils _jsonUtils; 079 080 /** The count of new tags **/ 081 private int _createdTagsCount; 082 /** The count of updated tags **/ 083 private int _updatedTagsCount; 084 /** The count of error **/ 085 private int _errorCount; 086 087 @Override 088 public void service(ServiceManager smanager) throws ServiceException 089 { 090 super.service(smanager); 091 _tagProviderExtPt = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 092 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 093 _targetTypeEP = (TagTargetTypeExtensionPoint) smanager.lookup(TagTargetTypeExtensionPoint.ROLE); 094 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 095 } 096 097 @Override 098 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 099 { 100 Map<String, Object> result = new HashMap<>(); 101 102 Request request = ObjectModelHelper.getRequest(objectModel); 103 104 String parentId = request.getParameter("tagId"); 105 String forceProvider = request.getParameter("tagProvider"); 106 if (StringUtils.isEmpty(forceProvider)) 107 { 108 forceProvider = CMSJCRTagProvider.class.getName(); 109 } 110 111 Part part = (Part) request.get("importFile"); 112 if (part instanceof RejectedPart) 113 { 114 return Collections.singletonMap("error", "rejected-file"); 115 } 116 117 PartOnDisk uploadedFilePart = (PartOnDisk) part; 118 File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null; 119 String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null; 120 121 if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS)) 122 { 123 result.put("success", false); 124 result.put("error", "invalid-extension"); 125 } 126 else if (uploadedFile == null) 127 { 128 result.put("success", false); 129 result.put("error", "no-file"); 130 } 131 else 132 { 133 String contextualParametersAsStr = request.getParameter("contextualParameters"); 134 Map<String, Object> contextualParameters = _jsonUtils.convertJsonToMap(contextualParametersAsStr); 135 136 Configuration configuration = initializeTags(uploadedFile.getPath()); 137 Map<String, CMSTag> tags = configureTags(configuration, null, "plugin.cms"); 138 139 _createdTagsCount = 0; 140 _updatedTagsCount = 0; 141 _errorCount = 0; 142 143 DefaultTraversableAmetysObject<?> parent = _resolver.resolveById(parentId); 144 createOrUpdateTags(parent, tags, contextualParameters); 145 146 CMSJCRTagProvider provider = (CMSJCRTagProvider) _tagProviderExtPt.getExtension(forceProvider); 147 TraversableAmetysObject rootNode = provider.getRootNode(contextualParameters); 148 149 result.put("success", true); 150 result.put("rootId", rootNode.getId()); 151 result.put("createdCount", _createdTagsCount); 152 result.put("updatedCount", _updatedTagsCount); 153 result.put("errorCount", _errorCount); 154 } 155 156 request.setAttribute(JSonReader.OBJECT_TO_READ, result); 157 return EMPTY_MAP; 158 } 159 160 /** 161 * Save or update a tag 162 * @param parent the JCR parent tag or provider 163 * @param tags the list of tags to add or update 164 * @param contextualParameters Contextual parameters transmitted by the environment. 165 */ 166 protected void createOrUpdateTags(DefaultTraversableAmetysObject<?> parent, Map<String, CMSTag> tags, Map<String, Object> contextualParameters) 167 { 168 boolean update = false; 169 for (CMSTag cmsTag : tags.values()) 170 { 171 CMSJCRTag jcrTag = null; 172 173 try 174 { 175 if (parent.hasChild(cmsTag.getName())) 176 { 177 // Update existing tag 178 jcrTag = parent.getChild(cmsTag.getName()); 179 update = true; 180 } 181 else 182 { 183 // Create new tag 184 String name = _findUniqueName (cmsTag.getId(), contextualParameters); 185 jcrTag = parent.createChild(name, CMSTagFactory.TAG_NODETYPE); 186 } 187 188 jcrTag.setTitle(cmsTag.getTitle().getLabel()); 189 jcrTag.setDescription(cmsTag.getDescription().getLabel()); 190 jcrTag.setVisibility(cmsTag.getVisibility()); 191 jcrTag.setTargetType(cmsTag.getTarget()); 192 193 // recursive call to save/update 194 createOrUpdateTags(jcrTag, cmsTag.getTags(), contextualParameters); 195 196 jcrTag.saveChanges(); 197 198 Map<String, Object> eventParams = new HashMap<>(); 199 eventParams.put(ObservationConstants.ARGS_TAG_ID, jcrTag.getId()); 200 eventParams.put(ObservationConstants.ARGS_TAG_NAME, jcrTag.getName()); 201 202 if (update) 203 { 204 _observationManager.notify(new Event(ObservationConstants.EVENT_TAG_UPDATED, _getCurrentUser(), eventParams)); 205 _updatedTagsCount++; 206 } 207 else 208 { 209 // Notify observers that the tag has been added. 210 _observationManager.notify(new Event(ObservationConstants.EVENT_TAG_ADDED, _getCurrentUser(), eventParams)); 211 _createdTagsCount++; 212 } 213 } 214 catch (Exception e) 215 { 216 getLogger().error("Unable to add tag " + cmsTag.getName() + " to JCR tag category of id '" + parent.getId() + "'", e); 217 _errorCount++; 218 } 219 } 220 } 221 222 /** 223 * Initialize a configuration tags from the tags file. 224 * @param path the path of the file to initialize tags. 225 * @return The configuration 226 * @throws Exception if an error occurs. 227 */ 228 protected Configuration initializeTags(String path) throws Exception 229 { 230 try (InputStream is = new FileInputStream(path)) 231 { 232 SAXParserFactory factory = SAXParserFactory.newInstance(); 233 234 XMLReader reader = factory.newSAXParser().getXMLReader(); 235 236 DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader); 237 238 return confBuilder.build(is); 239 } 240 catch (IOException e) 241 { 242 return null; 243 } 244 } 245 246 /** 247 * Configure tag from the passed configuration 248 * @param configuration The configuration 249 * @param parent The parent tag if any 250 * @param defaultCatalogue The default catalogue for i18n 251 * @return a Set of {@link CMSTag} 252 * @throws ConfigurationException If an error occurred 253 */ 254 protected Map<String, CMSTag> configureTags (Configuration configuration, CMSTag parent, String defaultCatalogue) throws ConfigurationException 255 { 256 Map<String, CMSTag> tags = new HashMap<>(); 257 258 Configuration[] tagsConfiguration = configuration.getChildren("tag"); 259 for (Configuration tagConfiguration : tagsConfiguration) 260 { 261 String id = tagConfiguration.getAttribute("id"); 262 if (!Tag.NAME_PATTERN.matcher(id).matches()) 263 { 264 throw new ConfigurationException("Invalid tag ID '" + id + "': it must match the pattern " + Tag.NAME_PATTERN.toString(), configuration); 265 } 266 267 TagVisibility visibility = TagVisibility.PUBLIC; 268 if (tagConfiguration.getAttribute("private", "").equals("true")) 269 { 270 visibility = TagVisibility.PRIVATE; 271 } 272 273 String typeName = tagConfiguration.getAttribute("target", "CONTENT"); 274 TagTargetType targetType = _targetTypeEP.getTagTargetType(typeName); 275 276 I18nizableText label = configureLabel (tagConfiguration, defaultCatalogue); 277 I18nizableText description = configureDescription (tagConfiguration, defaultCatalogue); 278 CMSTag tag = new CMSTag(id, id, parent, label, description, visibility, targetType); 279 tags.put(id, tag); 280 281 // Recursive configuration 282 Map<String, CMSTag> childTags = configureTags(tagConfiguration, tag, defaultCatalogue); 283 tag.setTags(childTags); 284 } 285 286 return tags; 287 } 288 289 /** 290 * Configure label from the passed configuration 291 * @param configuration The configuration 292 * @param defaultCatalogue The default catalogue 293 * @return The label 294 * @throws ConfigurationException If an error occurred 295 */ 296 protected I18nizableText configureLabel (Configuration configuration, String defaultCatalogue) throws ConfigurationException 297 { 298 Configuration labelConfiguration = configuration.getChild("label"); 299 300 if (labelConfiguration.getAttributeAsBoolean("i18n", false)) 301 { 302 return new I18nizableText(defaultCatalogue, labelConfiguration.getValue("")); 303 } 304 else 305 { 306 return new I18nizableText(labelConfiguration.getValue("")); 307 } 308 } 309 310 /** 311 * Configure description from the passed configuration 312 * @param configuration The configuration 313 * @param defaultCatalogue The default catalogue 314 * @return The description 315 * @throws ConfigurationException If an error occurred 316 */ 317 protected I18nizableText configureDescription (Configuration configuration, String defaultCatalogue) throws ConfigurationException 318 { 319 Configuration descConfiguration = configuration.getChild("description"); 320 321 if (descConfiguration.getAttributeAsBoolean("i18n", false)) 322 { 323 return new I18nizableText(defaultCatalogue, descConfiguration.getValue("")); 324 } 325 else 326 { 327 return new I18nizableText(descConfiguration.getValue("")); 328 } 329 } 330 331 private String _findUniqueName(String originalName, Map<String, Object> contextualParameters) 332 { 333 Set<TagProvider<CMSTag>> providers = _getTagProviders(); 334 335 // Find an unique name 336 int index = 2; 337 String name = originalName; 338 while (_hasTag(providers, name, contextualParameters)) 339 { 340 name = originalName + "_" + (index++); 341 } 342 343 return name; 344 } 345 346 private boolean _hasTag(Set<TagProvider<CMSTag>> providers, String name, Map<String, Object> contextualParameters) 347 { 348 for (TagProvider<CMSTag> provider : providers) 349 { 350 if (provider.hasTag(name, contextualParameters)) 351 { 352 return true; 353 } 354 } 355 return false; 356 } 357 358 private Set<TagProvider<CMSTag>> _getTagProviders () 359 { 360 Set<TagProvider<CMSTag>> providers = new HashSet<>(); 361 362 Set<String> ids = _tagProviderExtPt.getExtensionsIds(); 363 for (String id : ids) 364 { 365 TagProvider<CMSTag> provider = _tagProviderExtPt.getExtension(id); 366 providers.add(provider); 367 } 368 369 return providers; 370 } 371}