001/* 002 * Copyright 2023 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 */ 016 017package org.ametys.plugins.repository.jcr; 018 019import java.text.Normalizer; 020import java.util.regex.Matcher; 021import java.util.regex.Pattern; 022 023import org.apache.jackrabbit.util.Text; 024 025import org.ametys.core.util.StringUtils; 026import org.ametys.plugins.repository.TraversableAmetysObject; 027 028/** 029 * Helper for implementing {@link TraversableAmetysObject} stored in JCR. 030 */ 031public final class NameHelper 032{ 033 private static final Pattern __NODE_NAME_PATTERN = Pattern.compile("^([0-9-_]*)[a-z].*$"); 034 035 /** 036 * Mode of computation for the name if it already exists in JCR. 037 */ 038 public enum NameComputationMode 039 { 040 /** 041 * Use the legacy incremental mode: add a suffix which is automatically increment while the node name already exists. 042 * Prefix: [node name in lower case without special characters] 043 * Suffix: -[incremental number] 044 */ 045 INCREMENTAL, 046 047 /** 048 * Use {@link StringUtils}.generatedKey() method to create a random suffix. 049 * Prefix: [node name in lower case without special characters] 050 * Suffix: -[code with 8 lower-case alphanumerics characters] 051 */ 052 GENERATED_KEY, 053 054 /** 055 * Use for user-friendly pattern as: Non-formatted title (incremented number). 056 * Prefix: [given name] 057 * Suffix: [space character]([incremental number]) 058 */ 059 USER_FRIENDLY 060 } 061 062 // group 1 is based name, group 2 is not captured it is the suffix (optional) 063 064 // my-name-is-robby, my-name-is-robby-azerty12, my-name-is-robby-qwerty45 (group 1: my-name-is-robby) 065 private static final Pattern __NAME_GENERATED_KEY_PATTERN = Pattern.compile("^(.+?)(?:-[0-9a-z]{8})?$"); 066 // my-name-is-robby, my-name-is-robby-1, my-name-is-robby-2 (group 1: my-name-is-robby) 067 private static final Pattern __NAME_INCREMENTAL_PATTERN = Pattern.compile("^(.+?)(?:-[0-9]+)?$"); 068 // My name is Robby, My name is Robby (1), My name is Robby (2) (group 1: My name is Robby) 069 private static final Pattern __NAME_USER_FRIENDLY_PATTERN = Pattern.compile("^(.+?)(?: \\([0-9]+\\))?$"); 070 071 private NameHelper() 072 { 073 // empty 074 } 075 076 /** 077 * Filter a name for using it into an URI. 078 * @param name the name to filter. 079 * @return the name filtered. 080 */ 081 public static String filterName(String name) 082 { 083 // Use lower case 084 // then remove accents 085 // then replace contiguous spaces with one dash 086 // and finally remove non-alphanumeric characters except - 087 String filteredName = Normalizer.normalize(name.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 088 filteredName = filteredName.replaceAll("œ", "oe").replaceAll("æ", "ae").replaceAll(" +", "-").replaceAll("[^\\w-]", "-").replaceAll("-+", "-"); 089 090 Matcher m = __NODE_NAME_PATTERN.matcher(filteredName); 091 if (!m.matches()) 092 { 093 throw new IllegalArgumentException(filteredName + " doesn't match the expected regular expression : " + __NODE_NAME_PATTERN.pattern()); 094 } 095 096 filteredName = filteredName.substring(m.end(1)); 097 098 // Remove characters '-' and '_' at the start and the end of the string 099 return org.apache.commons.lang3.StringUtils.strip(filteredName, "-_"); 100 } 101 102 /** 103 * Get a unique child ametys object name under a traversable ametys object. Default parameters: Incremental mode, may not be suffixed. 104 * The name is filtered and suffixed if necessary. If a suffix is already present, it is removed to be replaced by another one. 105 * @param parent The parent 106 * @param baseName The base name 107 * @return A unique ametys object name (for the given parent) 108 */ 109 public static String getUniqueAmetysObjectName(TraversableAmetysObject parent, String baseName) 110 { 111 return getUniqueAmetysObjectName(parent, baseName, NameComputationMode.INCREMENTAL, false); 112 } 113 114 /** 115 * Get a unique child ametys object name under a traversable ametys object. 116 * The name is filtered and suffixed if necessary. If a suffix is already present, it is removed to be replaced by another one. 117 * @param parent The parent 118 * @param baseName The base name 119 * @param computationMode The computation mode of the name: incremental (old method) or generated key 120 * @param mayBeSuffixed <code>true</code> to consider that the given base name may already contains a suffix, it will be removed if needed 121 * @return A unique ametys object name (for the given parent) 122 */ 123 public static String getUniqueAmetysObjectName(TraversableAmetysObject parent, String baseName, NameComputationMode computationMode, boolean mayBeSuffixed) 124 { 125 return getUniqueAmetysObjectName(parent, baseName, computationMode, mayBeSuffixed, org.apache.commons.lang3.StringUtils.EMPTY); 126 } 127 128 /** 129 * Get a unique child ametys object name under a traversable ametys object. 130 * The name is filtered and suffixed if necessary. If a suffix is already present, it is removed to be replaced by another one. 131 * @param parent The parent 132 * @param baseName The base name 133 * @param computationMode The computation mode of the name: incremental (old method) or generated key 134 * @param mayBeSuffixed <code>true</code> to consider that the given base name may already contains a suffix, it will be removed if needed 135 * @param suffix The suffix to add to the name that we do not want to change, such as for file extensions 136 * @return A unique ametys object name (for the given parent) 137 */ 138 public static String getUniqueAmetysObjectName(TraversableAmetysObject parent, String baseName, NameComputationMode computationMode, boolean mayBeSuffixed, String suffix) 139 { 140 // Only compute the name if the base name already exists 141 switch (computationMode) 142 { 143 case GENERATED_KEY: 144 return _getUniqueAmetysObjectNameGeneratedKey(parent, baseName, mayBeSuffixed, suffix); 145 case USER_FRIENDLY: 146 return _getUniqueAmetysObjectNameUserFriendly(parent, Text.escapeIllegalJcrChars(baseName), mayBeSuffixed, suffix); 147 case INCREMENTAL: 148 default: 149 return _getUniqueAmetysObjectNameIncremental(parent, baseName, mayBeSuffixed, suffix); 150 } 151 } 152 153 private static String _getUniqueAmetysObjectNameIncremental(TraversableAmetysObject parent, String baseName, boolean mayBeSuffixed, String suffix) 154 { 155 String filteredName = filterName(baseName); 156 157 if (!parent.hasChild(filteredName)) 158 { 159 return filteredName; 160 } 161 162 String realBaseName = _getRealBaseName(baseName, __NAME_INCREMENTAL_PATTERN, mayBeSuffixed); 163 String uniqueName; 164 long index = 2; 165 do 166 { 167 uniqueName = realBaseName + (index++); 168 } 169 while (parent.hasChild(uniqueName + suffix)); 170 return uniqueName; 171 } 172 173 private static String _getUniqueAmetysObjectNameGeneratedKey(TraversableAmetysObject parent, String baseName, boolean mayBeSuffixed, String suffix) 174 { 175 String realBaseName = _getRealBaseName(baseName, __NAME_GENERATED_KEY_PATTERN, mayBeSuffixed); 176 String uniqueName; 177 do 178 { 179 uniqueName = realBaseName + StringUtils.generateKey().toLowerCase(); 180 } 181 while (parent.hasChild(uniqueName + suffix)); 182 return uniqueName + suffix; 183 } 184 185 private static String _getUniqueAmetysObjectNameUserFriendly(TraversableAmetysObject parent, String baseName, boolean mayBeSuffixed, String suffix) 186 { 187 if (!parent.hasChild(baseName + suffix)) 188 { 189 return baseName + suffix; 190 } 191 192 String realBaseName = baseName; 193 if (mayBeSuffixed) 194 { 195 Matcher m = __NAME_USER_FRIENDLY_PATTERN.matcher(baseName); 196 m.matches(); 197 realBaseName = m.group(1); 198 } 199 realBaseName += " (${number})"; 200 201 String uniqueName; 202 Long index = 2L; 203 do 204 { 205 uniqueName = realBaseName.replaceFirst("\\$\\{number\\}", index.toString()); 206 index++; 207 } 208 while (parent.hasChild(uniqueName + suffix)); 209 210 return uniqueName + suffix; 211 } 212 213 private static String _getRealBaseName(String baseName, Pattern pattern, boolean mayBeSuffixed) 214 { 215 if (!mayBeSuffixed) 216 { 217 return filterName(baseName) + "-"; 218 } 219 220 // "-" is not in the first group, because the second group is optional so it may be not present 221 // Add it there to avoid string computation 222 Matcher m = pattern.matcher(baseName); 223 m.matches(); 224 return filterName(m.group(1)) + "-"; 225 } 226}