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.String.format; 021import static java.util.Collections.emptyList; 022import static java.util.Collections.unmodifiableCollection; 023import static org.apiguardian.api.API.Status.MAINTAINED; 024import static org.tquadrat.foundation.lang.CommonConstants.CDATA_LEADIN; 025import static org.tquadrat.foundation.lang.CommonConstants.CDATA_LEADOUT; 026import static org.tquadrat.foundation.lang.Objects.nonNull; 027import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 028import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument; 029import static org.tquadrat.foundation.util.StringUtils.isEmpty; 030import static org.tquadrat.foundation.util.StringUtils.isNotEmpty; 031import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank; 032import static org.tquadrat.foundation.xml.builder.XMLBuilderUtils.getElementNameValidator; 033import static org.tquadrat.foundation.xml.builder.spi.SGMLPrinter.composeChildrenString; 034 035import java.util.ArrayList; 036import java.util.Collection; 037import java.util.HashSet; 038import java.util.List; 039import java.util.Optional; 040import java.util.function.Function; 041 042import org.apiguardian.api.API; 043import org.tquadrat.foundation.annotation.ClassVersion; 044import org.tquadrat.foundation.exception.IllegalOperationException; 045import org.tquadrat.foundation.util.LazyList; 046import org.tquadrat.foundation.xml.builder.internal.Comment; 047import org.tquadrat.foundation.xml.builder.internal.Text; 048 049/** 050 * This class provides the support for child elements and text to elements. 051 * As comments are also considered to be child elements, an element must have 052 * an instance of {@code ChildSupport} when it should take comments. If only 053 * comments should be allowed, the instance can be instantiated with 054 * {@link #ChildSupport(Element,boolean,boolean,boolean,Function) ChildSupport( parent, false, false, false, null );}.<br> 055 * The flag {@code checkValid} that applies to the constructors 056 * {@link #ChildSupport(Element,boolean)} 057 * and 058 * {@link #ChildSupport(Element,boolean,boolean,boolean,Function)} 059 * affects only child elements that are added through 060 * {@link #addChild(Element)}. 061 * 062 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 063 * @version $Id: ChildSupport.java 1071 2023-09-30 01:49:32Z tquadrat $ 064 * @since 0.0.5 065 * 066 * @UMLGraph.link 067 */ 068@ClassVersion( sourceVersion = "$Id: ChildSupport.java 1071 2023-09-30 01:49:32Z tquadrat $" ) 069@API( status = MAINTAINED, since = "0.0.5" ) 070public final class ChildSupport 071{ 072 /*-----------*\ 073 ====** Constants **======================================================== 074 \*-----------*/ 075 /** 076 * The message indicating that no children are allowed: {@value}. 077 */ 078 private static final String MSG_NoChildrenAllowed = "No children allowed for element '%1$s'"; 079 080 /*------------*\ 081 ====** Attributes **======================================================= 082 \*------------*/ 083 /** 084 * Flag that indicates whether child elements other than text are allowed. 085 */ 086 private final boolean m_AllowChildren; 087 088 /** 089 * Flag that indicates whether text is allowed. 090 */ 091 private final boolean m_AllowText; 092 093 /** 094 * Flag that indicates whether the validity of children should be 095 * checked. 096 */ 097 private final boolean m_CheckValid; 098 099 /** 100 * The list with the element's children. 101 */ 102 private final List<Element> m_Children; 103 104 /** 105 * The escape function that is used for text elements. 106 */ 107 private final Function<CharSequence,String> m_EscapeFunction; 108 109 /** 110 * The element that owns this {@code ChildSupport} instance. 111 */ 112 private final Element m_Owner; 113 114 /** 115 * The element names of valid children for a given element; the key for 116 * this map is the element name for the parent element. 117 */ 118 private final Collection<String> m_ValidChildren; 119 120 /*--------------*\ 121 ====** Constructors **===================================================== 122 \*--------------*/ 123 /** 124 * Creates a new {@code ChildSupport} instance for comments only. 125 * 126 * @param owner The element that owns this {@code ChildSupport} 127 * instance. 128 */ 129 public ChildSupport( final Element owner ) 130 { 131 this( owner, false, false, false, null ); 132 } // ChildSupport() 133 134 /** 135 * Creates a new {@code ChildSupport} instance that allows text, but no 136 * child elements. 137 * 138 * @param owner The element that owns this {@code ChildSupport} 139 * instance. 140 * @param escapeFunction The escape function that is used to convert 141 * special characters in texts; only required when {@code allowText} 142 * is {@code true}. 143 */ 144 public ChildSupport( final Element owner, final Function<CharSequence,String> escapeFunction ) 145 { 146 this( owner, false, false, true, escapeFunction ); 147 } // ChildSupport() 148 149 /** 150 * Creates a new {@code ChildSupport} instance that allows child elements, 151 * but no text. 152 * 153 * @param owner The element that owns this {@code ChildSupport} 154 * instance. 155 * @param checkValid {@code true} whether children are checked to be 156 * allowed before they are added. 157 */ 158 public ChildSupport( final Element owner, final boolean checkValid ) 159 { 160 this( owner, checkValid, true, false, null ); 161 } // ChildSupport() 162 163 /** 164 * Creates a new {@code ChildSupport} instance. 165 * 166 * @param owner The element that owns this {@code ChildSupport} 167 * instance. 168 * @param checkValid {@code true} whether children are checked to be 169 * allowed before they are added. 170 * @param allowChildren {@code true} if other elements could be added 171 * as children, {@code false} otherwise. 172 * @param allowText {@code true} it text could be added to the element, 173 * {@code false} if not. 174 * @param escapeFunction The escape function that is used to convert 175 * special characters in texts; only required when {@code allowText} 176 * is {@code true}. 177 */ 178 @SuppressWarnings( "BooleanParameter" ) 179 public ChildSupport( final Element owner, final boolean checkValid, final boolean allowChildren, final boolean allowText, final Function<CharSequence,String> escapeFunction ) 180 { 181 m_Owner = requireNonNullArgument( owner, "owner" ); 182 m_CheckValid = checkValid; 183 m_AllowChildren = allowChildren; 184 m_AllowText = allowText; 185 m_EscapeFunction = m_AllowText ? requireNonNullArgument( escapeFunction, "escapeFunction" ) : null; 186 187 m_Children = LazyList.use( ArrayList::new ); 188 m_ValidChildren = m_CheckValid ? new HashSet<>() : null; 189 } // ChildSupport() 190 191 /*---------*\ 192 ====** Methods **========================================================== 193 \*---------*/ 194 /** 195 * Adds a {@code CDATA} element. As {@code CDATA} is basically text, this 196 * is controlled by the same flag as text, too. 197 * 198 * @param text The text for the {@code CDATA} sequence. 199 * @throws IllegalOperationException No text is allowed for the owner. 200 */ 201 public final void addCDATA( final CharSequence text ) throws IllegalOperationException 202 { 203 addText( requireNonNullArgument( text, "text" ), ChildSupport::toCDATA, true ); 204 } // addCDATA() 205 206 /** 207 * Adds a child. 208 * 209 * @param <E> The implementation type for the {@code child}. 210 * @param child The child to add. 211 * @throws IllegalArgumentException The child is not allowed for the 212 * owner of this instance of {@code ChildSupport}. 213 * @throws IllegalStateException The child has already a parent that is 214 * not the owner of this instance of {@code ChildSupport}. 215 * @throws IllegalOperationException No children allowed for this 216 * element. 217 */ 218 public final <E extends Element> void addChild( final E child ) throws IllegalArgumentException, IllegalStateException, IllegalOperationException 219 { 220 addChildElement( "addChild()", child ); 221 } // addChild() 222 223 /** 224 * Adds a child element. 225 * 226 * @param <E> The implementation type for the {@code child}. 227 * @param operationName The name of the operation that was originally 228 * called. 229 * @param child The child to add. 230 * @throws IllegalArgumentException The child is not allowed for the 231 * owner of this instance of {@code ChildSupport}. 232 * @throws IllegalStateException The child has already a parent that is 233 * not the owner of this instance of {@code ChildSupport}. 234 * @throws IllegalOperationException No children allowed for this 235 * element. 236 */ 237 private final <E extends Element> void addChildElement( final String operationName, final E child ) throws IllegalArgumentException, IllegalStateException, IllegalOperationException 238 { 239 //---* Check if valid ... *-------------------------------------------- 240 checkValid( requireNonNullArgument( child, "child" ), requireNotEmptyArgument( operationName, "operationName" ) ); 241 242 //---* Add the child *------------------------------------------------- 243 m_Children.add( child ); 244 child.setParent( m_Owner ); 245 } // addChild() 246 247 /** 248 * Adds a comment. 249 * 250 * @param comment The comment text. 251 */ 252 public final void addComment( final CharSequence comment ) 253 { 254 if( isNotEmptyOrBlank( comment ) ) addChildElement( "addComment()", new Comment( comment ) ); 255 } // addComment() 256 257 /** 258 * <p>{@summary Adds predefined markup.}</p> 259 * <p>The given markup will not be validated, it just may not be 260 * {@code null}. So the caller is responsible that it will be proper 261 * markup.</p> 262 * <p>As the markup may be formatted differently (or not formatted at 263 * all), the pretty printed output may be distorted when this is used.</p> 264 * 265 * @param markup The predefined markup. 266 * @throws IllegalArgumentException The child is not allowed for the 267 * owner of this instance of {@code ChildSupport}. 268 * @throws IllegalOperationException No children allowed for this 269 * element. 270 */ 271 public final void addPredefinedMarkup( final CharSequence markup ) throws IllegalArgumentException, IllegalOperationException 272 { 273 requireNonNullArgument( markup, "markup" ); 274 275 final var operationName = "addPredefinedMarkup()"; 276 if( !allowsChildren() ) throw new IllegalOperationException( operationName, format( MSG_NoChildrenAllowed, m_Owner.getElementName() ) ); 277 addChildElement( operationName, new Text( markup, CharSequence::toString, true ) ); 278 } // addPredefinedMarkup() 279 280 /** 281 * Adds text. Special characters will be escaped by the escape function 282 * given with the 283 * {@linkplain #ChildSupport(Element, boolean, boolean, boolean, Function) constructor}. 284 * 285 * @param text The text. 286 * @throws IllegalArgumentException No text is allowed for the owner. 287 */ 288 public final void addText( final CharSequence text ) throws IllegalArgumentException 289 { 290 addText( requireNonNullArgument( text, "text" ), m_EscapeFunction, false ); 291 } // addText() 292 293 /** 294 * Adds text. 295 * 296 * @param text The text. 297 * @param escapeFunction The function the escapes the text in compliance 298 * with the type. 299 * @param addEmpty If {@code true} a new 300 * {@link Text} instance will be added even when the given 301 * {@code text} is empty, {@code false} means that empty {@code text} 302 * will be omitted. 303 * @throws IllegalOperationException No text is allowed for the owner. 304 */ 305 private final void addText( final CharSequence text, final Function<? super CharSequence, String> escapeFunction, final boolean addEmpty ) throws IllegalOperationException 306 { 307 assert nonNull( text ) : "text is null"; 308 assert !m_AllowText || nonNull( escapeFunction ) : "escapeFunction is null"; 309 310 if( !allowsText() ) throw new IllegalOperationException( "addText()", "No text allowed for element '%1$s'".formatted( m_Owner.getElementName() ) ); 311 if( addEmpty || isNotEmpty( text ) ) addChild( new Text( text, escapeFunction ) ); 312 } // addText() 313 314 /** 315 * Returns the flag that indicates whether this instance of 316 * {@code ChildSupport} allows other 317 * {@linkplain Element elements} 318 * to be added as children. 319 * 320 * @return {@code true} when child elements are allowed, {@code false} if 321 * not. 322 */ 323 @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" ) 324 public final boolean allowsChildren() { return m_AllowChildren; } 325 326 /** 327 * Returns the flag that indicates whether this instance of 328 * {@code ChildSupport} allows that text and {@code CDATA} elements can 329 * be added. 330 * 331 * @return {@code true} if it is allowed to add text and {@code CDATA}, 332 * {@code false} otherwise. 333 */ 334 @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" ) 335 public final boolean allowsText() { return m_AllowText; } 336 337 /** 338 * <p>{@summary Checks whether a child is valid for the element that owns 339 * this {@code ChildSupport} instance.}</p> 340 * <p>The child is valid either when 341 * {@link #m_CheckValid checkValid} 342 * is {@code false}, 343 * the child is a 344 * {@link Comment} 345 * or 346 * {@link Text}, 347 * {@link #m_ValidChildren} 348 * does not contain an entry for the 349 * {@linkplain #m_Owner owner's} 350 * {@linkplain Element#getElementName() element name}, 351 * or the child's element name is explicitly configured. Obviously, it is 352 * not valid, when no children (other then text or comments) are allowed 353 * at all.</p> 354 * 355 * @param child The child to check for. 356 * @param operationName The name of the attempted operation. 357 * @throws IllegalArgumentException The child is not allowed for the 358 * owner. 359 * @throws IllegalOperationException No children allowed for the owner. 360 */ 361 private final void checkValid( final Element child, @SuppressWarnings( "SameParameterValue" ) final String operationName ) throws IllegalArgumentException, IllegalOperationException 362 { 363 if( !(child instanceof Comment) && !(child instanceof Text) ) 364 { 365 if( !allowsChildren() ) throw new IllegalOperationException( operationName, format( MSG_NoChildrenAllowed, m_Owner.getElementName() ) ); 366 if( checksIfValid() ) 367 { 368 if( !m_ValidChildren.contains( child.getElementName() ) ) 369 { 370 throw new IllegalArgumentException( "A child with name '%2$s' is not allowed for element '%1$s'".formatted( m_Owner.getElementName(), child.getElementName() ) ); 371 } 372 } 373 } 374 375 final Optional<? extends Element> parent = child.getParent(); 376 if( parent.isPresent() ) 377 { 378 if( parent.get() != m_Owner ) throw new IllegalStateException( "The child has already a parent" ); 379 throw new IllegalStateException( "The child was already added to this parent" ); 380 } 381 } // checkValid() 382 383 /** 384 * Returns a flag that indicates whether an extended validity check is 385 * performed on child elements before adding them. 386 * 387 * @return {@code true} if extended validation are performed, 388 * {@code false} if any instance of 389 * {@link Element} 390 * can be added. Also {@code false} if no children are allowed at all. 391 * 392 * @see #addChild(Element) 393 * @see #allowsChildren() 394 */ 395 @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" ) 396 public final boolean checksIfValid() { return m_AllowChildren && m_CheckValid; } 397 398 /** 399 * Provides access to the children for this element; the returned 400 * collection is not modifiable. 401 * 402 * @return A reference the children of this element; if the element does 403 * not have children, an empty collection will be returned. 404 */ 405 public final Collection<? extends Element> getChildren() { return unmodifiableCollection( m_Children ); } 406 407 /** 408 * Returns {@code true} if the element has children, {@code false} 409 * otherwise. 410 * 411 * @return {@code true} if the element has children. 412 */ 413 public final boolean hasChildren() { return !m_Children.isEmpty(); } 414 415 /** 416 * Registers the element names of valid child elements for the owning 417 * element. 418 * 419 * @note The given children will be <i>added</i> to the already existing 420 * ones! 421 * 422 * @param children The element names of the valid children. 423 */ 424 public final void registerChildren( final String... children ) 425 { 426 if( m_CheckValid ) 427 { 428 for( final var child : requireNonNullArgument( children, "children" ) ) 429 { 430 if( !getElementNameValidator().test( child ) ) throw new InvalidXMLNameException( child ); 431 m_ValidChildren.add( child ); 432 } 433 } 434 } // registerChildren() 435 436 /** 437 * Returns the list of the registered children. 438 * 439 * @return The registered children. 440 */ 441 public final Collection<String> retrieveValidChildren() 442 { 443 final Collection<String> retValue = checksIfValid() ? List.copyOf( m_ValidChildren ) : emptyList(); 444 445 //---* Done *---------------------------------------------------------- 446 return retValue; 447 } // retrieveValidChildren() 448 449 /** 450 * {@summary "Escapes" the given String to a {@code CDATA} 451 * sequence.} 452 * 453 * @param text The text. 454 * @return The {@code CDATA} sequence. 455 */ 456 private static final String toCDATA( final CharSequence text ) 457 { 458 final var retValue = new StringBuilder(); 459 460 if( isEmpty( text ) ) 461 { 462 retValue.append( CDATA_LEADIN ) 463 .append( CDATA_LEADOUT ); 464 } 465 else 466 { 467 final var str = text.toString(); 468 var start = 0; 469 int pos; 470 //noinspection NestedAssignment 471 while( (pos = str.indexOf( "]", start )) >= 0 ) 472 { 473 if( pos == start ) 474 { 475 retValue.append( ']' ); 476 ++start; 477 } 478 else 479 { 480 retValue.append( CDATA_LEADIN ) 481 .append( str, start, pos ) 482 .append( CDATA_LEADOUT ) 483 .append( ']' ); 484 start = pos + 1; 485 } 486 } 487 if( start < text.length() ) 488 { 489 retValue.append( CDATA_LEADIN ) 490 .append( str.substring( start ) ) 491 .append( CDATA_LEADOUT ); 492 } 493 } 494 495 //---* Done *---------------------------------------------------------- 496 return retValue.toString(); 497 } // toCDATA() 498 499 /** 500 * Returns the children as a single formatted string. 501 * 502 * @param indentationLevel The indentation level. 503 * @param prettyPrint The pretty print flag. 504 * @return The children string. 505 */ 506 public final String toString( final int indentationLevel, final boolean prettyPrint ) 507 { 508 final var retValue = composeChildrenString( indentationLevel, prettyPrint, m_Owner, getChildren() ); 509 510 //---* Done *---------------------------------------------------------- 511 return retValue; 512 } // toString() 513} 514// class ChildSupport 515 516/* 517 * End of File 518 */