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