001/*
002 * ============================================================================
003 * Copyright © 2002-2023 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 *
007 * Licensed to the public under the agreements of the GNU Lesser General Public
008 * License, version 3.0 (the "License"). You may obtain a copy of the License at
009 *
010 *      http://www.gnu.org/licenses/lgpl.html
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015 * License for the specific language governing permissions and limitations
016 * under the License.
017 */
018
019package org.tquadrat.foundation.lang.internal;
020
021import static org.apiguardian.api.API.Status.INTERNAL;
022import static org.tquadrat.foundation.lang.Objects.isNull;
023import static org.tquadrat.foundation.lang.Objects.nonNull;
024import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
025import static org.tquadrat.foundation.lang.StringConverter.METHOD_NAME_GetSubjectClass;
026import static org.tquadrat.foundation.lang.StringConverter.METHOD_NAME_Provider;
027
028import java.lang.reflect.InvocationTargetException;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.Optional;
034import java.util.ServiceLoader;
035
036import org.apiguardian.api.API;
037import org.tquadrat.foundation.annotation.ClassVersion;
038import org.tquadrat.foundation.annotation.UtilityClass;
039import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
040import org.tquadrat.foundation.exception.UnexpectedExceptionError;
041import org.tquadrat.foundation.lang.StringConverter;
042
043/**
044 *  The implementation for the
045 *  {@link StringConverter}
046 *  service methods.
047 *
048 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
049 *  @version $Id: StringConverterService.java 1060 2023-09-24 19:21:40Z tquadrat $
050 *  @since 0.1.0
051 *
052 *  @UMLGraph.link
053 */
054@ClassVersion( sourceVersion = "$Id: StringConverterService.java 1060 2023-09-24 19:21:40Z tquadrat $" )
055@API( status = INTERNAL, since = "0.1.0" )
056@UtilityClass
057public final class StringConverterService
058{
059        /*--------------*\
060    ====** Constructors **=====================================================
061        \*--------------*/
062    /**
063     *  No instance allowed for this class.
064     */
065    private StringConverterService() { throw new PrivateConstructorForStaticClassCalledError( StringConverterService.class ); }
066
067        /*---------*\
068    ====** Methods **==========================================================
069        \*---------*/
070    /**
071     *  Returns the classes for that an instance of {@code StringConverter} is
072     *  registered,
073     *
074     *  @return The classes with a string converter.
075     */
076    @API( status = INTERNAL, since = "0.1.0" )
077    public static final Collection<Class<?>> listInstances()
078    {
079        final var registry = loadConverters();
080        final Collection<Class<?>> retValue = List.copyOf( registry.keySet() );
081
082        //---* Done *----------------------------------------------------------
083        return retValue;
084    }   //  listInstances()
085
086    /**
087     *  Loads the known instances of
088     *  {@link StringConverter}.
089     *
090     *  @return The registry with the converters.
091     */
092    @API( status = INTERNAL, since = "0.1.0" )
093    public static final Map<Class<?>,StringConverter<?>> loadConverters()
094    {
095        final var moduleLayer = StringConverter.class.getModule().getLayer();
096        final var converters = isNull( moduleLayer )
097            ? ServiceLoader.load( StringConverter.class )
098            : ServiceLoader.load( moduleLayer, StringConverter.class );
099
100        final Map<Class<?>,StringConverter<?>> retValue = new HashMap<>();
101
102        for( final StringConverter<?> converter : converters )
103        {
104            StringConverter<?> effectiveConverter;
105            try
106            {
107                final var providerMethod = converter.getClass().getMethod( METHOD_NAME_Provider );
108                effectiveConverter = (StringConverter<?>) providerMethod.invoke( null );
109            }
110            catch( final NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored )
111            {
112                effectiveConverter = converter;
113            }
114
115            for( final var subjectClass : retrieveSubjectClasses( effectiveConverter ) )
116            {
117                retValue.put( subjectClass, effectiveConverter );
118            }
119        }
120
121        //---* Done *----------------------------------------------------------
122        return retValue;
123    }   //  loadConverters()
124
125    /**
126     *  Returns an instance of {@code StringConverter} for the given
127     *  {@link Class}.
128     *  If there is no converter for the given type, or the type is
129     *  {@code null}, the return value is
130     *  {@link Optional#empty()}.
131     *
132     *  @param  <C> The class a converter is needed for.
133     *  @param  type    The instance of the class a converter is needed for.
134     *  @return An instance of
135     *      {@link Optional}
136     *      that holds the instance of {@code StringConverter}.
137     */
138    @API( status = INTERNAL, since = "0.1.0" )
139    public static final <C> Optional<StringConverter<C>> retrieveConverterForClass( final Class<C> type )
140    {
141        StringConverter<?> result = null;
142        if( nonNull( type ) )
143        {
144            final var registry = loadConverters();
145            result = registry.get( type );
146        }
147        @SuppressWarnings( "unchecked" )
148        final var retValue = Optional.ofNullable( (StringConverter<C>) result );
149
150        //---* Done *----------------------------------------------------------
151        return retValue;
152    }   //  retrieveConverterForClass()
153
154    /**
155     *  Returns an instance of {@code StringConverter} for the given
156     *  {@link Enum} type.
157     *  If there is no converter for the given type in the registry, a new
158     *  instance of {@code StringConverter} will be created, on base of
159     *  {@link DefaultEnumStringConverter}.
160     *
161     *  @param  <E> The class a converter is needed for.
162     *  @param  type    The instance of the class a converter is needed for.
163     *  @return The requested instance of {@code StringConverter}.
164     */
165    @API( status = INTERNAL, since = "0.1.0" )
166    public static final <E extends Enum<E>> StringConverter<E> retrieveConverterForEnum( final Class<E> type )
167    {
168        final var retValue = retrieveConverterForClass( requireNonNullArgument( type, "type" ) ).orElseGet( () -> new DefaultEnumStringConverter<>( type ) );
169
170        //---* Done *----------------------------------------------------------
171        return retValue;
172    }   //  retrieveConverterForEnum()
173
174    /**
175     *  <p>{@summary Determines the key class for the given instance of
176     *  {@link StringConverter}.}</p>
177     *  <p>The subject class is the target type for a call to
178     *  {@link StringConverter#fromString(CharSequence) fromString()},
179     *  and usually this is also the type of the argument for
180     *  {@link StringConverter#toString(Object) toString(T)};
181     *  but under some circumstances, it cannot be guessed correctly by
182     *  reflection. For that case, some implementation of
183     *  {@code StringConverter} provides an optional method
184     *  {@code public Class<?> getSubjectClass()} that returns the respective
185     *  class.</p>
186     *
187      * @param  converter   The converter instance.
188     *  @return The subject class.
189     */
190    @SuppressWarnings( {"NestedTryStatement", "unchecked"} )
191    private static final Collection<Class<?>> retrieveSubjectClasses( final StringConverter<?> converter )
192    {
193        final var converterClass = requireNonNullArgument( converter, "converter" ).getClass();
194        Collection<Class<?>> retValue;
195        try
196        {
197            try
198            {
199                final var getSubjectClassMethod = converterClass.getMethod( METHOD_NAME_GetSubjectClass );
200                retValue = (Collection<Class<?>>) getSubjectClassMethod.invoke( converter );
201            }
202            catch( final NoSuchMethodException ignored )
203            {
204                final var fromStringMethod = converterClass.getMethod( "fromString", CharSequence.class );
205                retValue = List.of( fromStringMethod.getReturnType() );
206            }
207        }
208        catch( final NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e )
209        {
210            throw new UnexpectedExceptionError( e );
211        }
212
213        //---* Done *----------------------------------------------------------
214        return retValue;
215    }   //  retrieveSubjectClass()
216}
217//  class StringConverterService
218
219/*
220 *  End of File
221 */