001/* 002 * ============================================================================ 003 * Copyright © 2002-2023 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.xml.builder.spi; 019 020import static java.lang.Integer.signum; 021import static java.lang.String.format; 022import static java.util.Collections.unmodifiableSortedMap; 023import static java.util.Comparator.naturalOrder; 024import static org.apiguardian.api.API.Status.MAINTAINED; 025import static org.tquadrat.foundation.lang.CommonConstants.XMLATTRIBUTE_Id; 026import static org.tquadrat.foundation.lang.CommonConstants.XMLATTRIBUTE_Language; 027import static org.tquadrat.foundation.lang.CommonConstants.XMLATTRIBUTE_Whitespace; 028import static org.tquadrat.foundation.lang.Objects.nonNull; 029import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 030import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument; 031import static org.tquadrat.foundation.util.Comparators.listBasedComparator; 032import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank; 033import static org.tquadrat.foundation.xml.builder.XMLBuilderUtils.getAttributeNameValidator; 034import static org.tquadrat.foundation.xml.builder.spi.SGMLPrinter.composeAttributesString; 035 036import java.util.Collection; 037import java.util.Comparator; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Optional; 043import java.util.SortedMap; 044import java.util.TreeMap; 045 046import org.apiguardian.api.API; 047import org.tquadrat.foundation.annotation.ClassVersion; 048import org.tquadrat.foundation.util.LazyMap; 049 050/** 051 * <p>{@summary This class provides the support for attributes to 052 * elements.}</p> 053 * <p>For some SGML elements, their attributes should be ordered in a given 054 * sequence, either because of convenience or because of deficits of the 055 * parser processing them.</p> 056 * <p>This class provides a specific comparator for each named element that 057 * can be configured by the user.</p> 058 * 059 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 060 * @version $Id: AttributeSupport.java 1071 2023-09-30 01:49:32Z tquadrat $ 061 * @since 0.0.5 062 * 063 * @UMLGraph.link 064 */ 065@ClassVersion( sourceVersion = "$Id: AttributeSupport.java 1071 2023-09-30 01:49:32Z tquadrat $" ) 066@API( status = MAINTAINED, since = "0.0.5" ) 067public final class AttributeSupport extends NamespaceSupport 068{ 069 /*-----------*\ 070 ====** Constants **======================================================== 071 \*-----------*/ 072 /** 073 * The default comparator that is used for attribute ordering if no other 074 * comparator is provided. 075 */ 076 private static final Comparator<String> DEFAULT_COMPARATOR = naturalOrder(); 077 078 /** 079 * A 080 * {@link Comparator} 081 * that ensures that 082 * {@value org.tquadrat.foundation.lang.CommonConstants#XMLATTRIBUTE_Id} 083 * is always the first attribute. 084 */ 085 @SuppressWarnings( {"IfStatementWithTooManyBranches", "OverlyLongLambda"} ) 086 public static final Comparator<String> ID_ALWAYS_FIRST_COMPARATOR = (a1,a2) -> 087 { 088 var retValue = 0; 089 if( a1.equals( a2 ) ) 090 { 091 retValue = 0; 092 } 093 else if( a1.equals( XMLATTRIBUTE_Id ) ) 094 { 095 retValue = -1; 096 } 097 else if( a2.equals( XMLATTRIBUTE_Id ) ) 098 { 099 retValue = 1; 100 } 101 else 102 { 103 retValue = signum( a1.compareTo( a2 ) ); 104 } 105 106 //---* Done *---------------------------------------------------------- 107 return retValue; 108 }; 109 110 /*------------*\ 111 ====** Attributes **======================================================= 112 \*------------*/ 113 /** 114 * The attributes for the element. 115 */ 116 private final Map<String,String> m_Attributes; 117 118 /** 119 * Flag that indicates whether the validity of attributes should be 120 * checked. 121 */ 122 private final boolean m_CheckValid; 123 124 /** 125 * The comparator that determines the sequence for the attributes of the 126 * owning element. 127 */ 128 private Comparator<String> m_Comparator; 129 130 /** 131 * The valid attributes for owning element. 132 */ 133 private final Collection<String> m_ValidAttributes; 134 135 /*--------------*\ 136 ====** Constructors **===================================================== 137 \*--------------*/ 138 /** 139 * Creates a new {@code AttributeSupport} instance that checks whether 140 * attributes are valid to be added. 141 * 142 * @param owner The element that owns this {@code AttributeSupport} 143 * instance. 144 */ 145 public AttributeSupport( final Element owner ) 146 { 147 this( owner, true, DEFAULT_COMPARATOR ); 148 } // AttributeSupport() 149 150 /** 151 * Creates a new {@code AttributeSupport} instance. 152 * 153 * @param owner The element that owns this {@code AttributeSupport} 154 * instance. 155 * @param checkValid {@code true} when the validity of attributes should 156 * be checked, {@code false} if all attributes can be added. 157 */ 158 public AttributeSupport( final Element owner, final boolean checkValid ) 159 { 160 this( owner, checkValid, DEFAULT_COMPARATOR ); 161 } // AttributeSupport() 162 163 /** 164 * Creates a new {@code AttributeSupport} instance that checks whether 165 * attributes are valid to be added. 166 * 167 * @param owner The element that owns this {@code AttributeSupport} 168 * instance. 169 * @param sortOrder The comparator that determines the sort order for 170 * the attribute of the owning element. 171 */ 172 public AttributeSupport( final Element owner, final Comparator<String> sortOrder ) 173 { 174 this( owner, true, sortOrder ); 175 } // AttributeSupport() 176 177 /** 178 * Creates a new {@code AttributeSupport} instance. 179 * 180 * @param owner The element that owns this {@code AttributeSupport} 181 * instance. 182 * @param checkValid {@code true} when the validity of attributes should 183 * be checked, {@code false} if all attributes can be added. 184 * @param sortOrder The comparator that determines the sort order for 185 * the attribute of the owning element. 186 */ 187 public AttributeSupport( final Element owner, final boolean checkValid, final Comparator<String> sortOrder ) 188 { 189 super( owner ); 190 m_CheckValid = checkValid; 191 setSortOrder( sortOrder ); 192 m_Attributes = LazyMap.use( HashMap::new ); 193 m_ValidAttributes = m_CheckValid ? new HashSet<>() : null; 194 if( m_CheckValid ) 195 { 196 //---* The reserved attributes that are always valid *------------- 197 m_ValidAttributes.add( XMLATTRIBUTE_Id ); 198 m_ValidAttributes.add( XMLATTRIBUTE_Language ); 199 m_ValidAttributes.add( XMLATTRIBUTE_Whitespace ); 200 } 201 } // AttributeSupport() 202 203 /*---------*\ 204 ====** Methods **========================================================== 205 \*---------*/ 206 /** 207 * <p>{@summary Checks whether an attribute with the given name is valid 208 * for the owning element.}</p> 209 * <p>The attribute is valid if there is a respective entry in the list 210 * of valid attributes, or when 211 * {@link #checksIfValid()} 212 * returns {@code false}.</p> 213 * 214 * @param attribute The name of the attribute. 215 * @return {@code true} if the attribute is valid for the given element, 216 * {@code false} otherwise. 217 * @throws InvalidXMLNameException The attribute name is invalid. 218 */ 219 public final boolean checkValid( final String attribute ) throws InvalidXMLNameException 220 { 221 if( !getAttributeNameValidator().test( requireNotEmptyArgument( attribute, "attribute" ) ) ) throw new InvalidXMLNameException( attribute ); 222 223 final var retValue = !checksIfValid() || m_ValidAttributes.contains( attribute ); 224 225 //---* Done *---------------------------------------------------------- 226 return retValue; 227 } // checkValid() 228 229 /** 230 * Returns a flag that indicates whether an extended validity check is 231 * performed on attributes before adding them. 232 * 233 * @return {@code true} if extended validation are performed, 234 * {@code false} if attribute can be added. 235 * 236 * @see #setAttribute(String, CharSequence, Optional) 237 */ 238 @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" ) 239 public final boolean checksIfValid() { return m_CheckValid; } 240 241 /** 242 * Returns the value for the attribute with the given name. 243 * 244 * @param name The attribute name. 245 * @return An instance of 246 * {@link Optional} 247 * that holds the value for that attribute. 248 */ 249 public final Optional<String> getAttribute( final String name ) 250 { 251 final var retValue = Optional.ofNullable( m_Attributes.get( requireNotEmptyArgument( name, "name" ) ) ); 252 253 //---* Done *---------------------------------------------------------- 254 return retValue; 255 } // getAttribute() 256 257 /** 258 * Provides read access to the attributes. 259 * 260 * @return A reference to the attributes. 261 */ 262 public final Map<String,String> getAttributes() 263 { 264 final SortedMap<String,String> map = new TreeMap<>( m_Comparator ); 265 map.putAll( m_Attributes ); 266 final var retValue = unmodifiableSortedMap( map ); 267 268 //---* Done *---------------------------------------------------------- 269 return retValue; 270 } // getAttributes() 271 272 /** 273 * Returns the attribute sort order. 274 * 275 * @return The comparator that determines the attribute's sequence. 276 */ 277 @SuppressWarnings( "SuspiciousGetterSetter" ) 278 public final Comparator<String> getSortOrder() { return m_Comparator; } 279 280 /** 281 * <p>{@summary Registers the valid attributes for the owning 282 * element.}</p> 283 * <p>Nothing happens if 284 * {@link #checksIfValid()} 285 * returns {@code false}, although a call to this method is obsolete 286 * then.</p> 287 * 288 * @note The given attributes will be <i>added</i> to the already 289 * existing ones! 290 * 291 * @param attributes The names of the valid attributes. 292 * @throws InvalidXMLNameException One of the attribute names is invalid. 293 */ 294 public final void registerAttributes( final String... attributes ) 295 { 296 if( m_CheckValid ) 297 { 298 for( final var attribute : requireNonNullArgument( attributes, "attributes" ) ) 299 { 300 if( !getAttributeNameValidator().test( attribute ) ) throw new InvalidXMLNameException( attribute ); 301 m_ValidAttributes.add( attribute ); 302 } 303 } 304 } // registerAttributes() 305 306 /** 307 * <p>{@summary Registers an attribute sequence for the owning element; 308 * this modifies any sort order that was previously set.}</p> 309 * <p>The names for the attributes are not validated; in particular, it 310 * is not checked whether an attribute is listed as valid.</p> 311 * 312 * @param attributes The names of the attributes in the desired 313 * sequence. 314 */ 315 public final void registerSequence( final String... attributes ) 316 { 317 if( requireNonNullArgument( attributes, "attributes" ).length > 0 ) 318 { 319 final Comparator<String> comparator = listBasedComparator( s -> s, naturalOrder(), attributes ); 320 setSortOrder( comparator ); 321 } 322 } // registerSequence() 323 324 /** 325 * Returns the list of the registered attributes. 326 * 327 * @return The registered attributes. 328 */ 329 public final Collection<String> retrieveValidAttributes() { return List.copyOf( m_ValidAttributes ); } 330 331 /** 332 * <p>{@summary Sets the attribute with the given name.}</p> 333 * <p>The given attribute name is validated using the method that is 334 * provided by 335 * {@link org.tquadrat.foundation.xml.builder.XMLBuilderUtils#getAttributeNameValidator()}.</p> 336 * 337 * @param name The name of the attribute; the name is case-sensitive. 338 * @param value The attribute's value; if {@code null} the 339 * attribute will be removed. 340 * @param append If not 341 * {@linkplain Optional#empty() empty}, the new value will be appended 342 * on an already existing one, and this sequence is used as the 343 * separator. 344 * @return An instance of 345 * {@link Optional} 346 * that holds the former value of the attribute; will be 347 * {@link Optional#empty()} 348 * if the element did not have an attribute with the given name 349 * before. 350 * @throws IllegalArgumentException The attribute name is invalid or 351 * the attribute is not valid for the element that owns this instance 352 * of {@code AttributeSupport}. 353 */ 354 public final Optional<String> setAttribute( final String name, final CharSequence value, @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" ) final Optional<? extends CharSequence> append ) throws IllegalArgumentException 355 { 356 requireNonNullArgument( append, "append" ); 357 if( !checkValid( name ) ) throw new IllegalArgumentException( "Invalid attribute name: %s".formatted( name ) ); 358 359 //---* Get the current value for the given name *---------------------- 360 final var retValue = Optional.ofNullable( m_Attributes.get( name ) ); 361 362 if( nonNull( value ) ) 363 { 364 //---* Set the new value *----------------------------------------- 365 if( retValue.isEmpty() || append.isEmpty() ) 366 { 367 m_Attributes.put( name, value.toString() ); 368 } 369 else 370 { 371 final var oldValue = retValue.get(); 372 final var newValue = isNotEmptyOrBlank( oldValue ) ? format( "%1$s%3$s%2$s", oldValue, value, append.get() ) : value.toString(); 373 m_Attributes.replace( name, newValue ); 374 } 375 } 376 else 377 { 378 //---* Remove the value *------------------------------------------ 379 m_Attributes.remove( name ); 380 } 381 382 //---* Done *---------------------------------------------------------- 383 return retValue; 384 } // setAttribute() 385 386 /** 387 * Sets the comparator that determines the sequence of the attributes for 388 * the owning element. 389 * 390 * @param sortOrder The comparator. 391 */ 392 public final void setSortOrder( final Comparator<String> sortOrder ) 393 { 394 m_Comparator = requireNonNullArgument( sortOrder, "sortOrder" ); 395 } // setSortOrder() 396 397 /** 398 * Returns the attributes and their values, together with the namespaces, 399 * as a single formatted string. 400 * 401 * @param indentationLevel The indentation level. 402 * @param prettyPrint The pretty print flag. 403 * @return The attributes string. 404 */ 405 @Override 406 public final String toString( final int indentationLevel, final boolean prettyPrint ) 407 { 408 final var retValue = composeAttributesString( indentationLevel, prettyPrint, getOwner().getElementName(), getAttributes(), getNamespaces() ); 409 410 //---* Done *---------------------------------------------------------- 411 return retValue; 412 } // toString() 413} 414// class AttributeSupport 415 416/* 417 * End of File 418 */