001/*
002 * ============================================================================
003 *  Copyright © 2002-2024 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.inifile.internal;
019
020import static java.lang.Integer.signum;
021import static java.lang.String.format;
022import static java.util.stream.Collectors.joining;
023import static org.apiguardian.api.API.Status.INTERNAL;
024import static org.apiguardian.api.API.Status.MAINTAINED;
025import static org.tquadrat.foundation.inifile.internal.INIFileImpl.breakString;
026import static org.tquadrat.foundation.inifile.internal.INIFileImpl.splitComment;
027import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
028import static org.tquadrat.foundation.lang.Objects.nonNull;
029import static org.tquadrat.foundation.lang.Objects.requireNotBlankArgument;
030import static org.tquadrat.foundation.lang.Objects.requireValidArgument;
031import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
032
033import java.util.Collection;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Optional;
037import java.util.Set;
038import java.util.StringJoiner;
039import java.util.TreeMap;
040import java.util.function.Predicate;
041
042import org.apiguardian.api.API;
043import org.tquadrat.foundation.annotation.ClassVersion;
044
045/**
046 *  The group for an INI file.
047 *
048 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
049 *  @version $Id: Group.java 1105 2024-02-28 12:58:46Z tquadrat $
050 *
051 *  @UMLGraph.link
052 *  @since 0.1.0
053 */
054@SuppressWarnings( "NewClassNamingConvention" )
055@ClassVersion( sourceVersion = "$Id: Group.java 1105 2024-02-28 12:58:46Z tquadrat $" )
056@API( status = INTERNAL, since = "0.1.0" )
057public final class Group implements Comparable<Group>
058{
059        /*------------*\
060    ====** Attributes **=======================================================
061        \*------------*/
062    /**
063     *  The comment for this group.
064     */
065    @SuppressWarnings( "StringBufferField" )
066    private final StringBuilder m_Comment = new StringBuilder();
067
068    /**
069     *  The name of the group.
070     */
071    private final String m_Name;
072
073    /**
074     *  The values for this group.
075     */
076    private final Map<String,Value> m_Values = new TreeMap<>();
077
078        /*--------------*\
079    ====** Constructors **=====================================================
080        \*--------------*/
081    /**
082     *  Creates a new instance for {@code Group}.
083     *
084     *  @param  name    The name for the group.
085     */
086    public Group( final String name )
087    {
088        m_Name = requireValidArgument( name, "name", Group::checkGroupNameCandidate );
089    }   //  Group()
090
091        /*---------*\
092    ====** Methods **==========================================================
093        \*---------*/
094    /**
095     *  Adds a comment to the group.
096     *
097     *  @param  comment The comment.
098     */
099    public final void addComment( final String comment )
100    {
101        if( isNotEmptyOrBlank( comment ) ) m_Comment.append( comment );
102    }   //  addComment()
103
104    /**
105     *  Adds a comment to the value with the given key.
106     *
107     *  @param  key The key.
108     *  @param  comment The comment.
109     */
110    public final void addComment( final String key, final String comment )
111    {
112        requireValidArgument( key, "key", Value::checkKeyCandidate );
113        if( nonNull( comment ) )
114        {
115            m_Values.computeIfAbsent( key, this::createValue )
116                .addComment( comment );
117        }
118    }   //  addComment()
119
120    /**
121     *  <p>{@summary An implementation of
122     *  {@link Predicate }
123     *  to be used with
124     *  {@link org.tquadrat.foundation.lang.Objects#requireValidArgument(Object,String,Predicate)}
125     *  when checking the names for new groups.}</p>
126     *  <p>The candidate may not contain newline characters, tab characters or
127     *  closing brackets (']').</p>
128     *  <p>You should avoid equal signs ('='), hash signs ('#') and opening
129     *  brackets ('['), too, although they are technically allowed.</p>
130     *
131     *  @param  candidate   The key or name to check.
132     *  @return {@code true} if the value is a valid name or key.
133     *  @throws org.tquadrat.foundation.exception.NullArgumentException The
134     *      candidate is {@code null}.
135     *  @throws org.tquadrat.foundation.exception.EmptyArgumentException    The
136     *      candidate is the empty string.
137     *  @throws org.tquadrat.foundation.exception.BlankArgumentException    The
138     *      candidate consists of whitespace only.
139     *
140     *  @since 0.4.4
141     */
142    @API( status = INTERNAL, since = "0.4.4" )
143    public static final boolean checkGroupNameCandidate( final String candidate )
144    {
145        var retValue = requireNotBlankArgument( candidate, "candidate" ).indexOf( ']' ) < 0;
146        if( retValue ) retValue = candidate.indexOf( '\n' ) < 0;
147        if( retValue ) retValue = candidate.indexOf( '\t' ) < 0;
148
149        //---* Done *----------------------------------------------------------
150        return retValue;
151    }   //  checkGroupNameCandidate()
152
153    /**
154     *  {@inheritDoc}
155     *  <p>This method is different from
156     *  {@link #equals(Object) equals()}
157     *  as it does not consider the comment.</p>
158     *
159     *  @since 0.4.2
160     */
161    @Override
162    @API( status = MAINTAINED, since = "0.4.2" )
163    public final int compareTo( final Group o )
164    {
165        final var retValue = signum( m_Name.compareTo( o.m_Name ) );
166
167        //---* Done *----------------------------------------------------------
168        return retValue;
169    }   //  compareTo()
170
171    /**
172     *  Creates a new instance of
173     *  {@link Value}
174     *  for the given name.
175     *
176     *  @param  key The name of the value.
177     *  @return The new instance.
178     */
179    private final Value createValue( final String key )
180    {
181        /*
182         * The key argument will be checked by the constructor itself.
183         */
184        final var retValue = new Value( this, key );
185
186        //---* Done *----------------------------------------------------------
187        return retValue;
188    }   //  createValue()
189
190    /**
191     *  {@inheritDoc}
192     */
193    @Override
194    public final boolean equals( final Object o )
195    {
196        var retValue = this == o;
197        if( !retValue && o instanceof final Group other )
198        {
199            retValue = m_Comment.toString().contentEquals( other.m_Comment )
200                && m_Name.equals( other.m_Name )
201                && m_Values.equals( other.m_Values );
202        }
203
204        //---* Done *----------------------------------------------------------
205        return retValue;
206    }   //  equals()
207
208    /**
209     *  Returns all keys for this group.
210     *
211     *  @return The keys.
212     */
213    public final Collection<String> getKeys()
214    {
215        final var retValue = Set.copyOf( m_Values.keySet() );
216
217        //---* Done *----------------------------------------------------------
218        return retValue;
219    }   //  getKeys()
220
221    /**
222     *  Returns the name for this group.
223     *
224     *  @return The group's name.
225     */
226    public final String getName() { return m_Name; }
227
228    /**
229     *  Returns the value for the given key from this group.
230     *
231     *  @param  key The key.
232     *  @return An instance of
233     *      {@link Optional}
234     *      that holds the value.
235     */
236    public final Optional<Value> getValue( final String key )
237    {
238        final var retValue = Optional.ofNullable( m_Values.get( requireValidArgument( key, "key", Value::checkKeyCandidate ) ) );
239
240        //---* Done *----------------------------------------------------------
241        return retValue;
242    }   //  getValue()
243
244    /**
245     *  {@inheritDoc}
246     */
247    @Override
248    public final int hashCode() { return Objects.hash( m_Comment, m_Name, m_Values ); }
249
250    /**
251     *  <p>{@summary Sets a comment to the group.}</p>
252     *  <p>Any previously existing comment will be overwritten.</p>
253     *
254     *  @param  comment The comment.
255     *
256     *  @since 0.4.2
257     */
258    @API( status = INTERNAL, since = "0.4.3" )
259    public final void setComment( final String comment )
260    {
261        m_Comment.setLength( 0 );
262        addComment( comment );
263    }   //  setComment()
264
265    /**
266     *  <p>{@summary Sets a comment to the value with the given key.}</p>
267     *  <p>Any previously existing comment will be overwritten.</p>
268     *
269     *  @param  key The key.
270     *  @param  comment The comment.
271     *
272     *  @since 0.4.2
273     */
274    @API( status = INTERNAL, since = "0.4.3" )
275    public final void setComment( final String key, final String comment )
276    {
277        final var value = m_Values.computeIfAbsent( requireValidArgument( key, "key", Value::checkKeyCandidate ), this::createValue );
278        value.setComment( comment );
279    }   //  setComment()
280
281    /**
282     *  Sets the given value for the given key.
283     *
284     *  @param  key The key.
285     *  @param  value   The new value; can be {@code null}.
286     *  @return The instance of
287     *      {@link Value}
288     *      for the new value.
289     */
290    public final Value setValue( final String key, final String value )
291    {
292        final var retValue = m_Values.computeIfAbsent( requireValidArgument( key, "key", Value::checkKeyCandidate ), this::createValue );
293        retValue.setValue( value );
294
295        //---* Done *----------------------------------------------------------
296        return retValue;
297    }   //  setValue()
298
299    /**
300     *  {@inheritDoc}
301     */
302    @Override
303    public final String toString()
304    {
305        final var joiner = new StringJoiner( "\n", EMPTY_STRING, "\n" );
306        joiner.add( EMPTY_STRING );
307        if( !m_Comment.isEmpty() )
308        {
309            splitComment( m_Comment ).forEach( joiner::add );
310        }
311        breakString( format( "[%s]", m_Name ) ).forEach( joiner::add );
312        final var retValue = m_Values.values()
313            .stream()
314            .map( Value::toString )
315            .collect( joining( EMPTY_STRING, joiner.toString(), EMPTY_STRING ) );
316
317        //---* Done *----------------------------------------------------------
318        return retValue;
319    }   //  toString()
320}
321//  class Group
322
323/*
324 *  End of File
325 */