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;
019
020import static java.lang.Integer.signum;
021import static java.lang.String.format;
022import static org.apiguardian.api.API.Status.STABLE;
023import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
024import static org.tquadrat.foundation.lang.Objects.mapFromNull;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026
027import java.io.IOException;
028import java.nio.file.Path;
029import java.util.Collection;
030import java.util.Optional;
031import java.util.function.Supplier;
032
033import org.apiguardian.api.API;
034import org.tquadrat.foundation.annotation.ClassVersion;
035import org.tquadrat.foundation.inifile.internal.INIFileImpl;
036import org.tquadrat.foundation.lang.StringConverter;
037
038/**
039 *  The API for the access to Windows-style configuration files.
040 *
041 *  @note   Changes will not be persisted automatically!
042 *
043 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
044 *  @version $Id: INIFile.java 1128 2024-04-06 07:17:56Z tquadrat $
045 *
046 *  @UMLGraph.link
047 *  @since 0.1.0
048 */
049@SuppressWarnings( "ClassWithTooManyMethods" )
050@ClassVersion( sourceVersion = "$Id: INIFile.java 1128 2024-04-06 07:17:56Z tquadrat $" )
051@API( status = STABLE, since = "0.1.0" )
052public sealed interface INIFile
053    permits org.tquadrat.foundation.inifile.internal.INIFileImpl
054{
055        /*---------------*\
056    ====** Inner Classes **====================================================
057        \*---------------*/
058    /**
059     *  An entry for the INI file.
060     *
061     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
062     *  @version $Id: INIFile.java 1128 2024-04-06 07:17:56Z tquadrat $
063     *
064     *  @param  group   The group for the entry.
065     *  @param  key The key for the entry.
066     *  @param  value   The entry's value.
067     *
068     *  @UMLGraph.link
069     *  @since 0.1.0
070     */
071    @SuppressWarnings( {"InnerClassOfInterface", "NewClassNamingConvention"} )
072    public record Entry( String group, String key, String value ) implements Comparable<Entry>
073    {
074            /*---------*\
075        ====** Methods **======================================================
076            \*---------*/
077        /**
078         *  {@inheritDoc}
079         *
080         *  @since 0.4.2
081         */
082        @Override
083        @API( status = STABLE, since = "0.4.2" )
084        public int compareTo( final Entry o )
085        {
086            final Supplier<String> emptyString = () -> EMPTY_STRING;
087            var retValue = signum( mapFromNull( group, emptyString ).compareTo( mapFromNull( o.group, emptyString ) ) );
088            if( retValue == 0 ) retValue = signum( mapFromNull( key, emptyString ).compareTo( mapFromNull( o.key, emptyString ) ) );
089            if( retValue == 0 ) retValue = signum( mapFromNull( value, emptyString ).compareTo( mapFromNull( o.value, emptyString ) ) );
090
091            //---* Done *------------------------------------------------------
092            return retValue;
093        }   //  compare()
094
095        /**
096         *  {@inheritDoc}
097         */
098        @Override
099        public final String toString() { return format( "%s/%s = %s", group, key, value ); }
100
101        /**
102         *  Retrieves the value and translates it to the desired type.
103         *
104         *  @param  <T> The target type.
105         *  @param  stringConverter The implementation of
106         *      {@link StringConverter}
107         *      that is used to convert the stored value into the target type.
108         *  @return The value; can be {@code null}.
109         */
110        public final <T> T value( final StringConverter<? extends T> stringConverter )
111        {
112            final var retValue = requireNonNullArgument( stringConverter, "stringConverter" ).fromString( value() );
113
114            //---* Done *----------------------------------------------------------
115            return retValue;
116        }   //  value()
117    }
118    //  record Entry
119
120        /*-----------*\
121    ====** Constants **========================================================
122        \*-----------*/
123    /**
124     *  The line length for an INI file: {@value}.
125     */
126    public static final int LINE_LENGTH = 75;
127
128        /*---------*\
129    ====** Methods **==========================================================
130        \*---------*/
131    /**
132     *  <p>{@summary Adds the given comment to the INI file.}</p>
133     *  <p>The new comment will be appended to an already existing one.</p>
134     *
135     *  @param  comment The comment.
136     */
137    public void addComment( final String comment );
138
139    /**
140     *  <p>{@summary Adds the given comment to the given group.}</p>
141     *  <p>The new comment will be appended to an already existing one.</p>
142     *
143     *  @param  group   The group.
144     *  @param  comment The comment.
145     */
146    public void addComment( final String group, final String comment );
147
148    /**
149     *  <p>{@summary Adds the given comment to the value that is identified by
150     *  the given group and key.}</p>
151     *  <p>The new comment will be appended to an already existing one.</p>
152     *
153     *  @param  group   The group.
154     *  @param  key The key for the value.
155     *  @param  comment The comment.
156     */
157    public void addComment( final String group, final String key, final String comment );
158
159    /**
160     *  <p>{@summary Creates an empty INI file.} If the file already exists, it
161     *  will be overwritten without notice.</p>
162     *  <p>The given file is used to store the value on a call to
163     *  {@link #save()}.</p>
164     *
165     *  @param  file    The file.
166     *  @return The new instance.
167     *  @throws IOException The file cannot be created.
168     */
169    public static INIFile create( final Path file ) throws IOException
170    {
171        return INIFileImpl.create( file );
172    }   //  create()
173
174    /**
175     *  Retrieves the value for the given key from the given group.
176     *
177     *  @param  group   The group.
178     *  @param  key The key for the value.
179     *  @return An instance of
180     *      {@link Optional}
181     *      that holds the retrieved value.
182     */
183    public Optional<String> getValue( final String group, final String key );
184
185    /**
186     *  Retrieves the value for the given key from the given group.
187     *
188     *  @param  group   The group.
189     *  @param  key The key for the value.
190     *  @param  defaultValue    The value that will be returned if no other
191     *      value could be found; can be {@code null}.
192     *  @return The value.
193     */
194    public default String getValue( final String group, final String key, final String defaultValue )
195    {
196        final var retValue = getValue( group, key ).orElse( defaultValue );
197
198        //---* Done *----------------------------------------------------------
199        return retValue;
200    }   //  getValue()
201
202    /**
203     *  Retrieves the value for the given key from the given group.
204     *
205     *  @param  <T> The target type.
206     *  @param  group   The group.
207     *  @param  key The key for the value.
208     *  @param  stringConverter The implementation of
209     *      {@link StringConverter}
210     *      that is used to convert the stored value into the target type.
211     *  @return An instance of
212     *      {@link Optional}
213     *      that holds the retrieved value.
214     */
215    public <T> Optional<T> getValue( final String group, final String key, final StringConverter<? extends T> stringConverter );
216
217    /**
218     *  Retrieves the value for the given key from the given group.
219     *
220     *  @param  <T> The target type.
221     *  @param  group   The group.
222     *  @param  key The key for the value.
223     *  @param  stringConverter The implementation of
224     *      {@link StringConverter}
225     *      that is used to convert the stored value into the target type.
226     *  @param  defaultValue    The value that will be returned if no other
227     *      value could be found; can be {@code null}.
228     *  @return The value.
229     */
230    public default <T> T getValue( final String group, final String key, final StringConverter<T> stringConverter, final T defaultValue )
231    {
232        final var retValue = getValue( group, key, stringConverter ).orElse( defaultValue );
233
234        //---* Done *----------------------------------------------------------
235        return retValue;
236    }   //  getValue()
237
238    /**
239     *  <p>{@summary Checks whether the INI file contains a group with the
240     *  given name.}</p>
241     *  <p>This method will not throw an exception for an invalid group name;
242     *  instead it returns {@code false}.</p>
243     *
244     *  @param  group   The group.
245     *  @return {@code true} if there is a group with the given name,
246     *      {@code false} otherwise.
247     */
248    @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
249    public boolean hasGroup( final String group );
250
251    /**
252     *  <p>{@summary Checks whether the INI file contains an entry with the
253     *  given key.}</p>
254     *  <p>This method will not throw an exception for an invalid group name or
255     *  an invalid key; instead it returns {@code false}.</p>
256     *
257     *  @param  group   The group.
258     *  @param  key The key.
259     *  @return {@code true} if there is an entry with the given key,
260     *      {@code false} otherwise.
261     */
262    @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
263    public boolean hasValue( final String group, final String key );
264
265    /**
266     *  Returns all entries of the INI file.
267     *
268     *  @return The entries.
269     */
270    public Collection<Entry> listEntries();
271
272    /**
273     *  Loads the entries for the INI file.
274     *
275     *  @param  entries The entries.
276     */
277    public default void loadEntries( final Entry... entries )
278    {
279        for( final var entry : requireNonNullArgument( entries, "entries" ) )
280        {
281            setValue( entry.group(), entry.key(), entry.value() );
282        }
283    }   //  loadEntries()
284
285    /**
286     *  Loads the entries for the INI file.
287     *
288     *  @param  entries The entries.
289     */
290    public default void loadEntries( final Collection<Entry> entries )
291    {
292        loadEntries( entries.toArray( Entry []::new ) );
293    }   //  loadEntries()
294
295    /**
296     *  Opens the given INI file and reads its contents. If the file does not
297     *  exist yet, a new, empty file will be created.
298     *
299     *  @param  file    The file.
300     *  @return The new instance.
301     *  @throws IOException A problem occurred when reading the file.
302     */
303    public static INIFile open( final Path file ) throws IOException { return INIFileImpl.open( file ); }
304
305    /**
306     *  Re-reads the values.
307     *
308     *  @throws IOException A problem occurred when reading the file.
309     */
310    public void refresh() throws IOException;
311
312    /**
313     *  Saves the contents of the INI file to the file that was provided to
314     *  {@link #create(Path)}
315     *  or
316     *  {@link #open(Path)}.
317     *  This method has to be called to persist any changes made to the
318     *  contents of the INI file.
319     *
320     *  @throws IOException  A problem occurred when writing the contents to
321     *      the file.
322     */
323    public void save() throws IOException;
324
325    /**
326     *  <p>{@summary Sets the given comment to the INI file.}</p>
327     *  <p>The new comment will replace any already existing comment.</p>
328     *
329     *  @param  comment The comment.
330     *
331     *  @since 0.4.3
332     */
333    @API( status = STABLE, since = "0.4.3" )
334    public void setComment( final String comment );
335
336    /**
337     *  <p>{@summary Sets the given comment to the given group.}</p>
338     *  <p>The new comment will replace any already existing comment.</p>
339     *
340     *  @param  group   The group.
341     *  @param  comment The comment.
342     *
343     *  @since 0.4.3
344     */
345    @API( status = STABLE, since = "0.4.3" )
346    public void setComment( final String group, final String comment );
347
348    /**
349     *  <p>{@summary Sets the given comment to the value that is identified by
350     *  the given group and key.}</p>
351     *  <p>The new comment will replace any already existing comment.</p>
352     *
353     *  @param  group   The group.
354     *  @param  key The key for the value.
355     *  @param  comment The comment.
356     *
357     *  @since 0.4.3
358     */
359    @API( status = STABLE, since = "0.4.3" )
360    public void setComment( final String group, final String key, final String comment );
361
362    /**
363     *  Stores the given value with the given key to the given group.
364     *
365     *  @param  group   The group.
366     *  @param  key The key.
367     *  @param  value   The value.
368     */
369    public void setValue( final String group, final String key, final String value );
370
371    /**
372     *  Stores the given value with the given key to the given group.
373     *
374     *  @param  <S> The source type for the value.
375     *  @param  group   The group.
376     *  @param  key The key.
377     *  @param  value   The value.
378     *  @param  stringConverter The instance of
379     *      {@link StringConverter}
380     *      that is used to convert the value to a String.
381     */
382    public default <S> void setValue( final String group, final String key, final S value, final StringConverter<S> stringConverter )
383    {
384        setValue( group, key, requireNonNullArgument( stringConverter, "stringConverter" ).toString( value ) );
385    }   //  setValue()
386}
387//  interface INIFile
388
389/*
390 *  End of File
391 */