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