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.util;
019
020import static java.lang.Math.max;
021import static java.lang.String.format;
022import static java.lang.System.arraycopy;
023import static java.lang.System.currentTimeMillis;
024import static java.lang.System.getProperties;
025import static java.lang.System.getProperty;
026import static java.lang.System.nanoTime;
027import static java.lang.Thread.currentThread;
028import static java.net.InetAddress.getByName;
029import static java.util.Arrays.stream;
030import static java.util.Collections.list;
031import static java.util.Locale.ROOT;
032import static org.apiguardian.api.API.Status.DEPRECATED;
033import static org.apiguardian.api.API.Status.EXPERIMENTAL;
034import static org.apiguardian.api.API.Status.INTERNAL;
035import static org.apiguardian.api.API.Status.STABLE;
036import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_CPUARCHITECTURE;
037import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_OSNAME;
038import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_OSVERSION;
039import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_USER_NAME;
040import static org.tquadrat.foundation.lang.CommonConstants.TIME_DELTA_BEGINGREGORIAN2BEGINEPOCH;
041import static org.tquadrat.foundation.lang.Objects.isNull;
042import static org.tquadrat.foundation.lang.Objects.nonNull;
043import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
044import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
045import static org.tquadrat.foundation.util.StringUtils.splitString;
046
047import java.io.File;
048import java.math.BigInteger;
049import java.net.Inet4Address;
050import java.net.InetAddress;
051import java.net.NetworkInterface;
052import java.net.SocketException;
053import java.net.UnknownHostException;
054import java.time.Duration;
055import java.time.Instant;
056import java.time.ZoneId;
057import java.util.Collection;
058import java.util.HashMap;
059import java.util.HexFormat;
060import java.util.IllformedLocaleException;
061import java.util.List;
062import java.util.Locale;
063import java.util.Map;
064import java.util.Optional;
065import java.util.Random;
066import java.util.regex.Pattern;
067import java.util.regex.PatternSyntaxException;
068
069import org.apiguardian.api.API;
070import org.tquadrat.foundation.annotation.ClassVersion;
071import org.tquadrat.foundation.annotation.UtilityClass;
072import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
073import org.tquadrat.foundation.exception.ValidationException;
074import org.tquadrat.foundation.lang.CommonConstants;
075import org.tquadrat.foundation.lang.Lazy;
076
077/**
078 *  This class provides some system related helper and convenience
079 *  methods.
080 *
081 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
082 *  @version $Id: SystemUtils.java 1136 2024-05-30 18:25:38Z tquadrat $
083 *  @since 0.0.5
084 *
085 *  @UMLGraph.link
086 */
087@SuppressWarnings( {"ClassWithTooManyMethods", "OverlyComplexClass"} )
088@ClassVersion( sourceVersion = "$Id: SystemUtils.java 1136 2024-05-30 18:25:38Z tquadrat $" )
089@API( status = STABLE, since = "0.0.5" )
090@UtilityClass
091public final class SystemUtils
092{
093        /*---------------*\
094    ====** Inner Classes **====================================================
095        \*---------------*/
096    /**
097     *  The operating system families that are supported (or not) by Java.<br>
098     *  <br>Currently, we can distinguish only between Microsoft Windows,
099     *  UNIX/Linux and MacOX/OS-X.
100     *
101     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
102     *  @version $Id: SystemUtils.java 1136 2024-05-30 18:25:38Z tquadrat $
103     *  @since 0.0.6
104     *
105     *  @UMLGraph.link
106     *
107     *  @see SystemUtils#determineOperatingSystem()
108     */
109    @ClassVersion( sourceVersion = "$Id: SystemUtils.java 1136 2024-05-30 18:25:38Z tquadrat $" )
110    @API( status = STABLE, since = "0.0.6" )
111    public static enum OperatingSystem
112    {
113            /*------------------*\
114        ====** Enum Declaration **=============================================
115            \*------------------*/
116        /**
117         *  The operating system is OS-X
118         */
119        OSX,
120
121        /**
122         *  The operating system is UNIX/Linux.
123         */
124        UNIX,
125
126        /**
127         *  The operating system is Microsoft Windows.
128         */
129        WINDOWS,
130
131        /**
132         *  The current operating system is unknown to this library.
133         */
134        UNKNOWN;
135
136            /*---------*\
137        ====** Methods **======================================================
138            \*---------*/
139        /**
140         *  Returns the operating system architecture.
141         *
142         *  @return The architecture.
143         */
144        @SuppressWarnings( "static-method" )
145        public final String getArchitecture() {return getProperty( PROPERTY_CPUARCHITECTURE ); }
146
147        /**
148         *  Returns the name of the operating system.
149         *
150         *  @return The operating system name.
151         */
152        @SuppressWarnings( "static-method" )
153        public final String getName() { return getProperty( PROPERTY_OSNAME ); }
154
155        /**
156         *  Returns the name of the operating system.
157         *
158         *  @return The operating system name.
159         */
160        public final String getNameVersion() { return format( "%s %s (%s)", getName(), getVersion(), getArchitecture() ); }
161
162        /**
163         *  Returns the version of the operating system.
164         *
165         *  @return The version of the operating system.
166         */
167        @SuppressWarnings( "static-method" )
168        public final String getVersion() { return getProperty( PROPERTY_OSVERSION ); }
169    }
170    //  enum OperatingSystem
171
172        /*-----------*\
173    ====** Constants **========================================================
174        \*-----------*/
175    /**
176     *  The valid bits for a node id: {@value}.
177     *
178     *  @see #m_Node
179     */
180    @API( status = INTERNAL, since = "0.1.0" )
181    private static final long m_NodeIdBits = 0x0000FFFFFFFFFFFFL;
182
183    /**
184     *  The sign bit for a node id: {@value}.
185     *
186     *  @see #m_Node
187     */
188    @API( status = INTERNAL, since = "0.1.0" )
189    private static final long m_NodeIdSign = 0x0000010000000000L;
190
191    /**
192     *  The length of a String containing a MAC address: {@value}.
193     */
194    @API( status = STABLE, since = "0.0.5" )
195    public static final int MAC_ADDRESS_Size = 17;
196
197    /**
198     *  Factor for the conversion of milliseconds to nanoseconds.
199     */
200    private static final BigInteger ONE_MILLION = BigInteger.valueOf( 1_000_000 );
201
202    /**
203     *  Factor for the conversion of seconds to milliseconds.
204     */
205    private static final BigInteger ONE_THOUSAND = BigInteger.valueOf( 1_000 );
206
207    /**
208     *  The regular expression for a valid IPv4 address: {@value}.
209     */
210    @API( status = STABLE, since = "0.0.5" )
211    public static final String PATTERN_IPv4_ADDRESS = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
212
213    /**
214     *  <p>{@summary The name of the system property for the node id:
215     *  {@value}.}</p>
216     *  <p>The value has to be a positive number. It will be parsed by
217     *  {@link Long#decode(String)};
218     *  this means, it could be a decimal, a hex or an octal number – so be
219     *  careful with leading zeroes!</p>
220     *  <p>This system property is not necessarily configured; if missing, the
221     *  node id will be determined by the hardware address of the NIC.</p>
222     *
223     *  @see #getNodeId()
224     */
225    @API( status = STABLE, since = "0.1.0" )
226    public static final String PROPERTY_NODE_ID = "org.tquadrat.foundation.util.SystemUtils.NodeId";
227
228        /*------------*\
229    ====** Attributes **=======================================================
230        \*------------*/
231    /**
232     *  The location or node id.
233     */
234    private static Long m_Node = null;
235
236        /*------------------------*\
237    ====** Static Initialisations **===========================================
238        \*------------------------*/
239    /**
240     *  The adjustment for the values that are returned from
241     *  {@link System#nanoTime()}.
242     */
243    private static final BigInteger m_NanoAdjust;
244
245    /**
246     *  The operating system.
247     */
248    private static final Lazy<OperatingSystem> m_OperatingSystem;
249
250    /**
251     *  The pattern to check a string for an IPv4 address.
252     */
253    private static final Lazy<Pattern> m_PatternIPv4Address;
254    /**
255     *  The random number generator.
256     */
257    private static final Lazy<Random> m_Random;
258
259    static
260    {
261        //---* The node id *---------------------------------------------------
262        final var nodeId = Long.getLong( PROPERTY_NODE_ID );
263        if( nonNull( nodeId ) )
264        {
265            m_Node = Long.valueOf( (nodeId.longValue() & m_NodeIdBits) | m_NodeIdSign );
266        }
267
268        //---* The operating system *------------------------------------------
269        m_OperatingSystem = Lazy.use( SystemUtils::determineOperatingSystem );
270
271        //---* The pattern for the IP addresses *------------------------------
272        m_PatternIPv4Address = Lazy.use( () ->
273        {
274            try
275            {
276                return Pattern.compile( PATTERN_IPv4_ADDRESS );
277            }
278            catch( final PatternSyntaxException e )
279            {
280                throw new ExceptionInInitializerError( e );
281            }
282        } );
283
284        //---* The random number generator *-----------------------------------
285        m_Random = Lazy.use( Random::new );
286
287        //---* The time adjustment *-------------------------------------------
288        final var timeDelta = BigInteger.valueOf( TIME_DELTA_BEGINGREGORIAN2BEGINEPOCH ).multiply( ONE_THOUSAND ).multiply( ONE_MILLION );
289        final var nanoNow = BigInteger.valueOf( nanoTime() );
290        final var milliNow = BigInteger.valueOf( currentTimeMillis() ).multiply( ONE_MILLION );
291        m_NanoAdjust = milliNow.subtract( nanoNow ).add( timeDelta );
292    }
293
294        /*--------------*\
295    ====** Constructors **=====================================================
296        \*--------------*/
297    /**
298     *  No instance allowed for this class.
299     */
300    private SystemUtils() { throw new PrivateConstructorForStaticClassCalledError( SystemUtils.class ); }
301
302        /*---------*\
303    ====** Methods **==========================================================
304        \*---------*/
305    /**
306     *  <p>{@summary Converts an IP address that is given as a string into an
307     *  {@link InetAddress}
308     *  object.}</p>
309     *
310     *  TODO Currently this method do not process IPv6 addresses properly.
311     *
312     *  @param  ipAddress   The string with the IP address.
313     *  @return The {@code InetAddress} object.
314     *  @throws IllegalArgumentException    The given parameter is not a valid
315     *      IP address.
316     */
317    @API( status = EXPERIMENTAL, since = "0.0.5" )
318    public static final InetAddress convertIPAddress( final CharSequence ipAddress )
319    {
320        final var message = "IP address is invalid: %1$s";
321        InetAddress retValue = null;
322
323        final var arg = requireNotEmptyArgument( ipAddress, "ipAddress" ).toString();
324        if( m_PatternIPv4Address.get().matcher( arg ).matches() || arg.contains( ":" ) )
325        {
326            try
327            {
328                retValue = getByName( arg );
329            }
330            catch( final UnknownHostException e )
331            {
332                throw new IllegalArgumentException( format( message, arg ), e );
333            }
334        }
335        else
336        {
337            throw new IllegalArgumentException( format( message, arg ) );
338        }
339
340        //---* Done *----------------------------------------------------------
341        return retValue;
342    }   //  convertIPAddress()
343
344    /**
345     *  <p>{@summary Returns a pseudo node id; this is useful in cases a
346     *  machine does not have a network card (NIC) so that a MAC address could
347     *  not be obtained, or if the real node id should not be used.}</p>
348     *  <p>Each call to this method will return a new value.</p>
349     *
350     *  @return The node id.
351     */
352    @API( status = STABLE, since = "0.0.5" )
353    public static final long createPseudoNodeId()
354    {
355        final var retValue = (getRandom().nextLong() & m_NodeIdBits) | m_NodeIdSign;
356
357        //---* Done *----------------------------------------------------------
358        return retValue;
359    }   //  createPseudoNodeId()
360
361    /**
362     *  Creates the alias map for the old (deprecated) zone ids that are used
363     *  for the call to
364     *  {@link ZoneId#of(String, java.util.Map)}
365     *  to retrieve a
366     *  {@link ZoneId}
367     *  instance for the given zone id.
368     *
369     *  @return The alias map.
370     *
371     *  @since 0.0.6
372     *
373     *  @deprecated Use
374     *      {@link DateTimeUtils#createZoneIdAliasMap()}
375     *      instead.
376     */
377    @API( status = DEPRECATED, since = "0.0.6" )
378    @Deprecated( since = "0.4.0", forRemoval = true )
379    public static final Map<String,String> createZoneIdAliasMap()
380    {
381        return DateTimeUtils.createZoneIdAliasMap();
382    }   //  createZoneIdAliasMap()
383
384    /**
385     *  Returns the current time as nanoseconds since the beginning of the
386     *  Gregorian calendar (1582-10-15T00:00).
387     *
388     *  @return The current time in nanoseconds.
389     */
390    @API( status = STABLE, since = "0.0.5" )
391    public static final BigInteger currentTimeNanos()
392    {
393        final var retValue = m_NanoAdjust.add( BigInteger.valueOf( nanoTime() ) );
394
395        //---* Done *----------------------------------------------------------
396        return retValue;
397    }   //  currentTimeNanos()
398
399    /**
400     *  Determines an IP address of the machine this program is running on.
401     *  Usually this will be one of the addresses that is visible to the
402     *  outside. In addition, the method has a clear precedence for IPv4
403     *  addresses over IPv6 ones.
404     *
405     *  @return An instance of
406     *      {@link Optional}
407     *      that holds the IP address; it will be empty if really no network
408     *      adapter is active on this machine.
409     *  @throws SocketException Problems to retrieve the internet address.
410     */
411    @SuppressWarnings( "OverlyNestedMethod" )
412    @API( status = STABLE, since = "0.0.5" )
413    public static final Optional<InetAddress> determineIPAddress() throws SocketException
414    {
415        Optional<InetAddress> retValue = Optional.empty();
416
417        List<InetAddress> addresses;
418        ScanLoop: for( final var networkInterface : getNetworkInterfaces() )
419        {
420            /*
421             * Due to the nature of the scan logic, the returned address will
422             * always be that of the last NIC in sequence that is not the
423             * {@code loopback} interface.
424             */
425            if( networkInterface.isUp() )
426            {
427                addresses = list( networkInterface.getInetAddresses() );
428                AddressLoop: for( final var address : addresses )
429                {
430                    /*
431                     * The AddressLoop terminates early, when the first IPv4
432                     * address for the NIC was found.
433                     */
434                    if( networkInterface.isLoopback() )
435                    {
436                        /*
437                         * We take loopback if we do not get something
438                         * better ...
439                         */
440                        if( retValue.isEmpty() )
441                        {
442                            retValue = Optional.of( address );
443                            if( address instanceof Inet4Address ) break AddressLoop;
444                        }
445                        else if( !(retValue.get() instanceof Inet4Address) )
446                        {
447                            retValue = Optional.of( address );
448                            break AddressLoop;
449                        }
450                    }
451                    else
452                    {
453                        retValue = Optional.of( address );
454                        if( address instanceof Inet4Address ) break AddressLoop;
455                    }
456                }
457            }
458        }   //  ScanLoop:
459
460        //---* Done *----------------------------------------------------------
461        return retValue;
462    }   //  determineIPAddress()
463
464    /**
465     *  Determines all the IP addresses of the machine this program is running
466     *  on.
467     *
468     *  @return The IP addresses.
469     */
470    @API( status = STABLE, since = "0.0.5" )
471    public static final InetAddress [] determineIPAddresses()
472    {
473        final var retValue = getNetworkInterfaces().stream()
474            .flatMap( networkInterface -> list( networkInterface.getInetAddresses() ).stream() )
475            .toArray( InetAddress []::new );
476
477        //---* Done *----------------------------------------------------------
478        return retValue;
479    }   //  determineIPAddresses()
480
481    /**
482     *  Determines the operating system.
483     *
484     *  @return The operating system.
485     */
486    private static final OperatingSystem determineOperatingSystem()
487    {
488        /*
489         * TODO Add more criteria for each family. And perhaps it should be
490         * distinguished between Linux, BSD and Unix (and there between HP-UX
491         * and AIX) ...
492         */
493        var retValue = OperatingSystem.UNKNOWN;
494
495        /*
496         * All currently known MS-DOS and Windows versions and their file
497         * systems use the backslash as the separator for folders.
498         * They also use the semicolon as the PATH separator.
499         */
500        final var fileSeparator = File.separatorChar;
501        final var pathSeparator = File.pathSeparatorChar;
502        if( (fileSeparator == '\\') && (pathSeparator == ';') )
503        {
504            retValue = OperatingSystem.WINDOWS;
505        }
506        else if( (fileSeparator == '/') && (pathSeparator == ':') )
507        {
508            if( getProperty( PROPERTY_OSNAME ).toLowerCase( ROOT ).contains( "osx" ) )
509            {
510                retValue = OperatingSystem.OSX;
511            }
512            else
513            {
514                retValue = OperatingSystem.UNIX;
515            }
516        }
517
518        //---* Done *----------------------------------------------------------
519        return retValue;
520    }   //  determineOperatingSystem()
521
522    /**
523     *  Determines those IP addresses of the machine this program is running
524     *  on that are used to communicate with the outside world. This means that
525     *  only those <i>active</i> network interfaces are considered that are
526     *  <i>not</i> a {@code loopback} interface.
527     *
528     *  @return The IP addresses.
529     */
530    @API( status = STABLE, since = "0.0.5" )
531    public static final InetAddress [] determineOutboundIPAddresses()
532    {
533        final var retValue = getNetworkInterfaces().stream()
534            .filter( networkInterface ->
535            {
536                try
537                {
538                    return networkInterface.isUp() && !networkInterface.isLoopback();
539                }
540                catch( final SocketException ignored )
541                {
542                    return false;
543                }
544            } )
545            .flatMap( networkInterface -> list( networkInterface.getInetAddresses() ).stream() )
546            .toArray( InetAddress []::new );
547
548        //---* Done *----------------------------------------------------------
549        return retValue;
550    }   //  determineOutboundIPAddresses()
551
552    /**
553     *  Formats the given node id as a MAC address string.
554     *
555     *  @param  nodeId  The node id.
556     *  @return The MAC address string.
557     */
558    @API( status = STABLE, since = "0.0.5" )
559    public static final String formatNodeIdAsMAC( final long nodeId )
560    {
561        if( nodeId != (nodeId & m_NodeIdBits) )
562        {
563            throw new ValidationException( "Node id is invalid: %1$d".formatted( nodeId ) );
564        }
565
566        final var hexFormat = HexFormat.of();
567        final var bytes = hexFormat.parseHex( hexFormat.toHexDigits( nodeId, 12 ) );
568        final var retValue = HexFormat.ofDelimiter( "-" ).withUpperCase().formatHex( bytes );
569
570        //---* Done *----------------------------------------------------------
571        return retValue;
572    }   //  formatNodeIdAsMAC()
573
574    /**
575     *  Returns the MAC address for the current computer. In case the machine
576     *  will have more than one network card (NIC), the returned MAC is
577     *  selected in the same as for
578     *  {@link #getNodeId()}.
579     *
580     *  @return The MAC address.
581     */
582    @API( status = STABLE, since = "0.0.5" )
583    public static final String getMACAddress() { return formatNodeIdAsMAC( getNodeId() ); }
584
585    /**
586     *  Returns a
587     *  {@link Collection}
588     *  of all the network interfaces on this machine. Instead of throwing an
589     *  exception, the method will return an empty collection in case the
590     *  machine do not have network configured. Otherwise, the collection
591     *  contains at least one element, possibly representing a {@code loopback}
592     *  interface that only supports communication between entities on this
593     *  machine.
594     *
595     *  @return The
596     *      {@link NetworkInterface}s found on this machine.
597     *
598     *  @see java.net.NetworkInterface#getNetworkInterfaces()
599     */
600    @API( status = STABLE, since = "0.0.5" )
601    public static final Collection<NetworkInterface> getNetworkInterfaces()
602    {
603        Collection<NetworkInterface> retValue = List.of();
604        try
605        {
606            retValue = List.copyOf( list( java.net.NetworkInterface.getNetworkInterfaces() ) );
607        }
608        catch( final SocketException ignored )
609        {
610            /*
611             * There are no NICs on this machine, so we return the empty list.
612             */
613        }
614
615        //---* Done *----------------------------------------------------------
616        return retValue;
617    }   //  getNetworkInterfaces()
618
619    /**
620     *  Returns the unique id of this node.
621     *
622     *  @return The node id.
623     *
624     *  @see #PROPERTY_NODE_ID
625     */
626    @SuppressWarnings( "OverlyComplexMethod" )
627    @API( status = STABLE, since = "0.0.5" )
628    public static final long getNodeId()
629    {
630        if( isNull( m_Node ) )
631        {
632            final var interfaces = getNetworkInterfaces();
633            var randomGenerationRequired = interfaces.isEmpty();
634            if( !randomGenerationRequired )
635            {
636                var count = interfaces.size();
637                ScanLoop: for( final var nic : interfaces )
638                {
639                    try
640                    {
641                        if( (--count > 0) && (nic.isLoopback() || nic.isVirtual()) )
642                        {
643                            /*
644                             * We prefer a real hardware NIC if we can get one.
645                             */
646                            continue ScanLoop;
647                        }
648
649                        final var hardwareAddress = nic.getHardwareAddress();
650                        if( nonNull( hardwareAddress ) && (hardwareAddress.length > 0) )
651                        {
652                            m_Node = Long.valueOf( new BigInteger( hardwareAddress ).longValue() & m_NodeIdBits );
653                            break ScanLoop;
654                        }
655                    }
656                    catch( final SocketException ignored )
657                    {
658                        /*
659                         *  If a SocketException is caught here, this indicates
660                         *  that we have problems to determine the NICs. In
661                         *  this case, we have to create a random node id.
662                         */
663                        break ScanLoop;
664                    }
665                }   //  ScanLoop:
666
667                /*
668                 * If we still do not have a valid node id, it seems that none
669                 * of the NICs have a hardware address; may be that they are
670                 * NICs for a virtual machine or virtual NICs of some kind.
671                 * But is it possible to have a NIC without a MAC?
672                 */
673                randomGenerationRequired = isNull( m_Node );
674            }
675
676            if( randomGenerationRequired )
677            {
678                //---* We have to create a random location *-------------------
679                m_Node = Long.valueOf( createPseudoNodeId() );
680            }
681        }
682
683        final var retValue = m_Node.longValue();
684
685        //---* Done *----------------------------------------------------------
686        return retValue;
687    }   //  getNodeId()
688
689    /**
690     *  Returns the current operating system.
691     *
692     *  @return The operating system.
693     */
694    @API( status = STABLE, since = "0.0.6" )
695    public static final OperatingSystem getOperatingSystem() { return m_OperatingSystem.get(); }
696
697    /**
698     *  Returns the PID, the process id, for the VM this (the current) program
699     *  runs in.
700     *
701     *  @return The PID.
702     *  @throws UnsupportedOperationException   The implementation of
703     *      {@link ProcessHandle}
704     *      that is currently in use does not support the operation
705     *      {@link ProcessHandle#pid()}.
706     *
707     *  @see ProcessHandle#pid()
708     */
709    @API( status = STABLE, since = "0.0.5" )
710    public static final long getPID() throws UnsupportedOperationException
711    {
712        final var retValue = ProcessHandle.current().pid();
713
714        //---* Done *----------------------------------------------------------
715        return retValue;
716    }   //  getPID()
717
718    /**
719     *  Returns the system random number generator.
720     *
721     *  @return The random number generator.
722     */
723    @API( status = STABLE, since = "0.0.5" )
724    public static final Random getRandom() { return m_Random.get(); }
725
726    /**
727     *  Returns the current username from the system property
728     *  {@value CommonConstants#PROPERTY_USER_NAME}.
729     *
730     *  @return The username.
731     *
732     *  @since 0.4.8
733     */
734    @API( status = STABLE, since = "0.4.8" )
735    public static final String getUserName() { return getProperty( PROPERTY_USER_NAME ); }
736
737    /**
738     *  Returns the alias map; if not yet created, the alias map will be
739     *  created by a call to
740     *  {@link #createZoneIdAliasMap()}
741     *  and the result to that call will be cached for future calls.
742     *
743     *  @return The alias map.
744     *
745     *  @see #createZoneIdAliasMap()
746     *
747     *  @since 0.0.6
748     *
749     *  @deprecated Use
750     *      {@link DateTimeUtils#getZoneIdAliasMap()}
751     *      instead.
752     */
753    @API( status = DEPRECATED, since = "0.0.5" )
754    @Deprecated( since = "0.4.0", forRemoval = true )
755    public static final Map<String,String> getZoneIdAliasMap() { return DateTimeUtils.getZoneIdAliasMap(); }
756
757    /**
758     *  Checks whether the current system has a network interface installed.
759     *
760     *  @return {@code true} if the current system has a network interface
761     *      installed, {@code false} otherwise.
762     */
763    @API( status = STABLE, since = "0.0.5" )
764    public static final boolean hasNetworkInterface()
765    {
766        final var retValue = !getNetworkInterfaces().isEmpty();
767
768        //---* Done *----------------------------------------------------------
769        return retValue;
770    }   //  hasNetworkInterface()
771
772    /**
773     *  An implementation of
774     *  {@link Thread#sleep(long)}
775     *  that does not throw an exception in case it will be interrupted.
776     *
777     *  @note   While sleeping, the thread does not lose ownership of any
778     *      monitors.
779     *
780     *  @param  millis  The time to sleep in milliseconds.
781     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
782     *      terminated as planned.
783     */
784    @SuppressWarnings( {"BooleanMethodNameMustStartWithQuestion", "BusyWait"} )
785    @API( status = STABLE, since = "0.0.7" )
786    public static final boolean repose( final long millis )
787    {
788        var retValue = true;
789        final var endTimeMillis  = currentTimeMillis() + millis;
790        SleepLoop: while( currentTimeMillis() < endTimeMillis )
791        {
792            try
793            {
794                Thread.sleep( max( 0, endTimeMillis - currentTimeMillis() ) );
795                retValue = false;
796            }
797            catch( final InterruptedException ignored )
798            {
799               if( currentThread().isInterrupted() ) break SleepLoop;
800            }
801        }   //  SleepLoop:
802
803        //---* Done *----------------------------------------------------------
804        return retValue;
805    }   //  repose()
806
807    /**
808     *  An implementation of
809     *  {@link Thread#sleep(Duration)}
810     *  that does not throw an exception in case it will be interrupted.
811     *
812     *  @note   While sleeping, the thread does not lose ownership of any
813     *      monitors.
814     *
815     *  @param  duration    The time to sleep.
816     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
817     *      terminated as planned.
818     */
819    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
820    @API( status = STABLE, since = "0.0.7" )
821    public static final boolean repose( final Duration duration )
822    {
823        var retValue = true;
824        var remainingDuration = requireNonNullArgument( duration, "duration" );
825        var now = Instant.now();
826        final var endTime = now.plus( duration );
827        SleepLoop: while( now.isBefore( endTime ) )
828        {
829            try
830            {
831                Thread.sleep( remainingDuration );
832                retValue = false;
833            }
834            catch( final InterruptedException ignored )
835            {
836                if( currentThread().isInterrupted() ) break SleepLoop;
837                now = Instant.now();
838                remainingDuration = Duration.between( now, endTime );
839            }
840        }   //  SleepLoop:
841
842        //---* Done *----------------------------------------------------------
843        return retValue;
844    }   //  repose()
845
846    /**
847     *  An implementation of
848     *  {@link #sleepUntil(Instant)}
849     *  that does not throw an exception when interrupted.
850     *
851     *  @note   While sleeping, the thread does not lose ownership of any
852     *      monitors.
853     *
854     *  @param  until   The end time for the sleep period.
855     *  @return  {@code true} if the sleep was interrupted, {@code false} if it
856     *      terminated as planned.
857     */
858    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
859    @API( status = STABLE, since = "0.0.7" )
860    public static final boolean reposeUntil( final Instant until )
861    {
862        var retValue = true;
863        SleepLoop: while( Instant.now().isBefore( requireNonNullArgument( until, "until" ) ) )
864        {
865            /*
866             * It is intended that Instant::now will be called twice, returning
867             * different results. This will increase accuracy (ok, just
868             * marginally), without causing much harm – we want to wast time
869             * anyways.
870             */
871            try
872            {
873                Thread.sleep( Duration.between( Instant.now(), until ) );
874                retValue = false;
875            }
876            catch( final InterruptedException ignored )
877            {
878                if( currentThread().isInterrupted() ) break SleepLoop;
879            }
880        }
881
882        //---* Done *----------------------------------------------------------
883        return retValue;
884    }   //  reposeUntil()
885    /**
886     *  Retrieves an instance of
887     *  {@link Locale}
888     *  for the given locale name. This method will look up the requested
889     *  locale first in the locales returned by
890     *  {@link Locale#getAvailableLocales()}
891     *  before it will create a new instance of {@code Locale} using
892     *  {@link java.util.Locale.Builder Locale.Builder}.<br>
893     *  <br>The method will return
894     *  {@link Optional#empty()}
895     *  when the format of the given {@code localeName} is invalid.<br>
896     *  <br>If none of the already existing instances of {@code Locale} matches
897     *  the given name, the language part, the country part, the variant part
898     *  and the script part (if provided) are used to create a new instance. If
899     *  extensions are given, they will be ignored.<br>
900     *  <br>If the given locale name is empty or contains only whitespace, the
901     *  method will return
902     *  {@link Locale#ROOT}.
903     *
904     *  @param  localeName  The name of the locale.
905     *  @return An instance of
906     *      {@link Optional}
907     *      that holds the instance of {@code Locale} for the given name.
908     */
909    @SuppressWarnings( {"AssignmentToNull", "OverlyNestedMethod", "OverlyComplexMethod"} )
910    @API( status = STABLE, since = "0.0.6" )
911    public static final Optional<Locale> retrieveLocale( final CharSequence localeName )
912    {
913        final var name = requireNonNullArgument( localeName, "localeName" ).toString();
914        @SuppressWarnings( "OverlyLongLambda" )
915        final var locale = name.isBlank()
916            ? ROOT
917            : stream( Locale.getAvailableLocales() )
918                  .filter( currentLocale -> currentLocale.toString().equals( name ) )
919                  .findFirst()
920                  .orElseGet( () ->
921                  {
922                      Locale result = null;
923                      final var parts = splitString( name, '_' );
924                      try
925                      {
926                          final var builder = new Locale.Builder();
927                          ComposeSwitch: result = switch( parts.length )
928                          {
929                              case 1 -> builder.setLanguage( parts[0] ).build();
930                              case 2 -> builder.setLanguage( parts[0] ).setRegion( parts[1] ).build();
931                              case 3 -> {
932                                  builder.setLanguage( parts[0] ).setRegion( parts[1] );
933                                  if( parts[2].startsWith( "#" ) )
934                                  {
935                                      if( parts[2].length() == 4 )
936                                          builder.setScript( parts[2].substring( 1 ) );
937                                  }
938                                  else
939                                  {
940                                      builder.setVariant( parts[2] );
941                                  }
942                                  yield builder.build();
943                              }
944                              case 4 -> {
945                                  builder.setLanguage( parts[0] ).setRegion( parts[1] );
946                                  if( parts[2].startsWith( "#" ) )
947                                  {
948                                      builder.setScript( parts[2].substring( 1 ) );
949                                  }
950                                  else
951                                  {
952                                      builder.setVariant( parts[2] );
953                                      if( parts[3].startsWith( "#" ) )
954                                      {
955                                          if( parts[3].length() == 4 )
956                                              builder.setScript( parts[3].substring( 1 ) );
957                                      }
958                                  }
959                                  yield builder.build();
960                              }
961                              case 5 -> builder.setLanguage( parts[0] ).setRegion( parts[1] ).setVariant( parts[2] ).setScript( parts[3].substring( 1 ) ).build();
962                              default -> null; // Invalid input ...
963                          };    //  ComposeSwitch:
964                      }
965                      catch( final IllformedLocaleException ignored )
966                      {
967                          result = null;
968                      }
969                      return result;
970                  } );
971
972        final var retValue = Optional.ofNullable( locale );
973
974        //---* Done *----------------------------------------------------------
975        return retValue;
976    }   //  retrieveLocale()
977
978    /**
979     *  An implementation of
980     *  {@link Thread#sleep}
981     *  that takes an instance of
982     *  {@link Duration}
983     *  to determine the time to sleep.
984     *
985     *  @note   While sleeping, the thread does not lose ownership of any
986     *      monitors.
987     *
988     *  @param  duration    The time to sleep.
989     *  @throws InterruptedException    Another thread has interrupted the
990     *      current thread (that one executing {@code sleep()}. The
991     *      <i>interrupted status</i> of the current thread is cleared when
992     *      this exception is thrown.
993     *
994     *  @deprecated Use
995     *      {@link Thread#sleep(Duration)}
996     *      instead.
997     */
998    @API( status = DEPRECATED, since = "0.0.7" )
999    @Deprecated( since = "0.4.8", forRemoval = true )
1000    public static final void sleep( final Duration duration ) throws InterruptedException
1001    {
1002        final var milliseconds = requireNonNullArgument( duration, "duration" ).toMillis();
1003        Thread.sleep( milliseconds );
1004    }   //  sleep()
1005
1006    /**
1007     *  Causes the currently executing thread to sleep (temporarily cease
1008     *  execution) until the specified time, subject to the precision and
1009     *  accuracy of system timers and schedulers. The thread does not lose
1010     *  ownership of any monitors.
1011     *
1012     *  @note   While sleeping, the thread does not lose ownership of any
1013     *      monitors.
1014     *
1015     *  @param  until   The end time for the sleep.
1016     *  @throws InterruptedException    Another thread has interrupted the
1017     *      current thread (that one executing {@code sleep()}. The
1018     *      <i>interrupted status</i> of the current thread is cleared when
1019     *      this exception is thrown.
1020     *
1021     *  @see Thread#sleep(long)
1022     *
1023     *  @since 0.0.7
1024     */
1025    @API( status = STABLE, since = "0.0.7" )
1026    public static final void sleepUntil( final Instant until ) throws InterruptedException
1027    {
1028        final var milliseconds = max( 0, requireNonNullArgument( until, "until" ).toEpochMilli() - currentTimeMillis() );
1029        Thread.sleep( milliseconds );
1030    }   //  sleepUntil()
1031
1032    /**
1033     *  <p>{@summary Returns the
1034     *  {@linkplain System#getProperties() system properties}}
1035     *  as a
1036     *  {@link Map Map&lt;String,String&gt;}.</p>
1037     *  <p>{@link System#getProperties()}
1038     *  returns an instance of
1039     *  {@link java.util.Properties}, and that is implementing
1040     *  {@code Map<String,Object>}. Although it is unlikely that a value is
1041     *  <i>not</i> a String (if not impossible &hellip;), this method allows
1042     *  to enforce it.</p>
1043     *
1044     *  @note   A mere cast to {@code Map<String,String>} does not work
1045     *      &hellip;
1046     *
1047     *  @return An unmodifiable map with the system properties as Strings.
1048     */
1049    public static final Map<String,String> systemPropertiesAsStringMap()
1050    {
1051        final Map<String,String> map = new HashMap<>();
1052        final var properties = getProperties();
1053        for( final var key : properties.stringPropertyNames() )
1054        {
1055            map.put( key, properties.getProperty( key ) );
1056        }
1057        final var retValue = Map.copyOf( map );
1058
1059        //---* Done *----------------------------------------------------------
1060        return retValue;
1061    }   //  systemPropertiesAsStringMap()
1062
1063    /**
1064     *  <p>{@summary Translates a given MAC address into a numerical node
1065     *  id.}</p>
1066     *  <p>A MAC address is a string consisting of 12 hex digits, grouped by
1067     *  two, each group separated by a hyphen:
1068     *  <code><i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i>-<i>xx</i></code></p>
1069     *
1070     *  @param  macAddress  The MAC address to translate.
1071     *  @return The node id.
1072     */
1073    @API( status = STABLE, since = "0.0.5" )
1074    public static final long translateMACToNodeId( final String macAddress )
1075    {
1076        final var bytes = HexFormat.ofDelimiter( "-" ).withUpperCase().parseHex( requireNotEmptyArgument( macAddress, "macAddress" ) );
1077        if( bytes.length != 6 )
1078        {
1079            throw new ValidationException( "MAC address is invalid: %1$s".formatted( macAddress ) );
1080        }
1081        final var bytes2 = new byte [7];
1082        arraycopy( bytes, 0, bytes2, 1, 6 );
1083        final var retValue = new BigInteger( bytes2 ).longValue();
1084
1085        //---* Done *----------------------------------------------------------
1086        return retValue;
1087    }   //  translateMACToNodeId()
1088}
1089//  class SystemUtils
1090
1091/*
1092 *  End of File
1093 */