CCommon.java
/*
* @cond LICENSE
* ######################################################################################
* # LGPL License #
* # #
* # This file is part of the LightJason AgentSpeak(L++) #
* # Copyright (c) 2015-19, LightJason (info@lightjason.org) #
* # This program is free software: you can redistribute it and/or modify #
* # it under the terms of the GNU Lesser General Public License as #
* # published by the Free Software Foundation, either version 3 of the #
* # License, or (at your option) any later version. #
* # #
* # This program is distributed in the hope that it will be useful, #
* # but WITHOUT ANY WARRANTY; without even the implied warranty of #
* # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
* # GNU Lesser General Public License for more details. #
* # #
* # You should have received a copy of the GNU Lesser General Public License #
* # along with this program. If not, see http://www.gnu.org/licenses/ #
* ######################################################################################
* @endcond
*/
package org.lightjason.agentspeak.common;
import com.google.common.reflect.ClassPath;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.lightjason.agentspeak.action.IAction;
import org.lightjason.agentspeak.action.binding.CMethodAction;
import org.lightjason.agentspeak.action.binding.IAgentAction;
import org.lightjason.agentspeak.action.binding.IAgentActionFilter;
import org.lightjason.agentspeak.agent.IAgent;
import org.lightjason.agentspeak.error.CIllegalArgumentException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Stream;
/**
* class for any helper calls
*/
public final class CCommon
{
/**
* package name
**/
public static final String PACKAGEROOT = "org.lightjason.agentspeak";
/**
* logger
*/
private static final Logger LOGGER = CCommon.logger( CCommon.class );
/**
* language resource bundle
**/
private static final ResourceBundle LANGUAGE = ResourceBundle.getBundle(
MessageFormat.format( "{0}.{1}", PACKAGEROOT, "language" ),
Locale.getDefault(),
new CUTF8Control()
);
/**
* properties of the package
*/
private static final ResourceBundle PROPERTIES = ResourceBundle.getBundle(
MessageFormat.format( "{0}.{1}", PACKAGEROOT, "configuration" ),
Locale.getDefault(),
new CUTF8Control()
);
/**
* private ctor - avoid instantiation
*/
private CCommon()
{}
/**
* returns a logger instance
*
* @param p_class class type
* @return logger
*/
@Nonnull
public static Logger logger( final Class<?> p_class )
{
return Logger.getLogger( p_class.getName() );
}
/**
* list of usable languages
*
* @return list of language pattern
*/
@Nonnull
public static String[] languages()
{
return Arrays.stream( PROPERTIES.getString( "translation" ).split( "," ) ).map( i -> i.trim().toLowerCase() ).toArray( String[]::new );
}
/**
* returns the language bundle
*
* @return bundle
*/
@Nonnull
public static ResourceBundle languagebundle()
{
return LANGUAGE;
}
/**
* returns the property data of the package
*
* @return bundle object
*/
@Nonnull
public static ResourceBundle configuration()
{
return PROPERTIES;
}
// --- access to action instantiation ----------------------------------------------------------------------------------------------------------------------
/**
* get all classes within an Java package as action
*
* @param p_package full-qualified package name or empty for default package
* @return action stream
*/
@Nonnull
public static Stream<IAction> actionsFromPackage( @Nullable final String... p_package )
{
return ( ( Objects.isNull( p_package ) ) || ( p_package.length == 0 )
? Stream.of( MessageFormat.format( "{0}.{1}", PACKAGEROOT, "action.builtin" ) )
: Arrays.stream( p_package ) )
.flatMap( j ->
{
try
{
return ClassPath.from( Thread.currentThread().getContextClassLoader() )
.getTopLevelClassesRecursive( j )
.parallelStream()
.map( ClassPath.ClassInfo::load )
.filter( i -> !Modifier.isAbstract( i.getModifiers() ) )
.filter( i -> !Modifier.isInterface( i.getModifiers() ) )
.filter( i -> Modifier.isPublic( i.getModifiers() ) )
.filter( IAction.class::isAssignableFrom )
.map( i ->
{
try
{
return (IAction) i.getConstructor().newInstance();
}
catch ( final NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException l_exception )
{
LOGGER.warning( CCommon.languagestring( CCommon.class, "actioninstantiate", i, l_exception ) );
return null;
}
} )
// action can be instantiate
.filter( Objects::nonNull )
// check usable action name
.filter( CCommon::actionusable );
}
catch ( final IOException l_exception )
{
throw new UncheckedIOException( l_exception );
}
} );
}
/**
* returns actions by a class
* @note class must be an inheritance of the IAgent interface
*
* @param p_class class list
* @return action stream
*/
@Nonnull
@SuppressWarnings( "unchecked" )
public static Stream<IAction> actionsFromAgentClass( @Nonnull final Class<?>... p_class )
{
return p_class.length == 0
? Stream.empty()
: Arrays.stream( p_class )
.parallel()
.filter( IAgent.class::isAssignableFrom )
.flatMap( CCommon::methods )
.map( i ->
{
try
{
return (IAction) new CMethodAction( i );
}
catch ( final IllegalAccessException l_exception )
{
LOGGER.warning( CCommon.languagestring( CCommon.class, "actioninstantiate", i, l_exception ) );
return null;
}
} )
// action can be instantiate
.filter( Objects::nonNull )
// check usable action name
.filter( CCommon::actionusable );
}
/**
* checks if an action is usable
*
* @param p_action action object
* @return boolean usable flag
*/
private static boolean actionusable( final IAction p_action )
{
if ( ( p_action.name().empty() ) || ( p_action.name().get( 0 ).trim().isEmpty() ) )
{
LOGGER.warning( CCommon.languagestring( CCommon.class, "actionnameempty" ) );
return false;
}
if ( !Character.isLetter( p_action.name().get( 0 ).charAt( 0 ) ) )
{
LOGGER.warning( CCommon.languagestring( CCommon.class, "actionletter", p_action ) );
return false;
}
if ( !Character.isLowerCase( p_action.name().get( 0 ).charAt( 0 ) ) )
{
LOGGER.warning( CCommon.languagestring( CCommon.class, "actionlowercase", p_action ) );
return false;
}
return true;
}
/**
* reads all methods by the action-annotations
* for building agent-actions
*
* @param p_class class
* @return stream of all methods with inheritance
*/
@Nonnull
private static Stream<Method> methods( final Class<?> p_class )
{
final Pair<Boolean, IAgentAction.EAccess> l_classannotation = CCommon.isActionClass( p_class );
if ( !l_classannotation.getLeft() )
return Objects.isNull( p_class.getSuperclass() )
? Stream.empty()
: methods( p_class.getSuperclass() );
final Predicate<Method> l_filter = IAgentAction.EAccess.WHITELIST.equals( l_classannotation.getRight() )
? i -> !CCommon.isActionFiltered( i, p_class )
: i -> CCommon.isActionFiltered( i, p_class );
return Stream.concat(
Arrays.stream( p_class.getDeclaredMethods() )
.parallel()
.peek( i -> i.setAccessible( true ) )
.filter( i -> !Modifier.isAbstract( i.getModifiers() ) )
.filter( i -> !Modifier.isInterface( i.getModifiers() ) )
.filter( i -> !Modifier.isNative( i.getModifiers() ) )
.filter( i -> !Modifier.isStatic( i.getModifiers() ) )
.filter( l_filter ),
methods( p_class.getSuperclass() )
);
}
/**
* filter of a class to use it as action
*
* @param p_class class for checking
* @return boolean flag of check result
*/
@Nonnull
private static Pair<Boolean, IAgentAction.EAccess> isActionClass( final Class<?> p_class )
{
if ( !p_class.isAnnotationPresent( IAgentAction.class ) )
return new ImmutablePair<>( false, IAgentAction.EAccess.BLACKLIST );
final IAgentAction l_annotation = p_class.getAnnotation( IAgentAction.class );
return new ImmutablePair<>(
( l_annotation.classes().length == 0 )
|| ( Arrays.stream( p_class.getAnnotation( IAgentAction.class ).classes() )
.parallel()
.anyMatch( p_class::equals )
),
l_annotation.access()
);
}
/**
* class filter of an action to use it
*
* @param p_method method for checking
* @param p_class class
* @return boolean flag of check result
*/
private static boolean isActionFiltered( final Method p_method, final Class<?> p_class )
{
return p_method.isAnnotationPresent( IAgentActionFilter.class )
&& (
( p_method.getAnnotation( IAgentActionFilter.class ).classes().length == 0 )
|| ( Arrays.stream( p_method.getAnnotation( IAgentActionFilter.class ).classes() )
.parallel()
.anyMatch( p_class::equals )
)
);
}
// --- resource access -------------------------------------------------------------------------------------------------------------------------------------
/**
* concats an URL with a path
*
* @param p_base base URL
* @param p_string additional path
* @return new URL
*
* @throws URISyntaxException thrown on syntax error
* @throws MalformedURLException thrown on malformat
*/
@Nonnull
public static URL concaturl( final URL p_base, final String p_string ) throws MalformedURLException, URISyntaxException
{
return new URL( p_base.toString() + p_string ).toURI().normalize().toURL();
}
/**
* returns root path of the resource
*
* @return URL of file or null
*/
@Nullable
public static URL resourceurl()
{
return CCommon.class.getClassLoader().getResource( "" );
}
/**
* returns a file from a resource e.g. Jar file
*
* @param p_file file
* @return URL of file or null
*
* @throws URISyntaxException thrown on syntax error
* @throws MalformedURLException thrown on malformat
*/
@Nonnull
public static URL resourceurl( final String p_file ) throws URISyntaxException, MalformedURLException
{
return resourceurl( new File( p_file ) );
}
/**
* returns a file from a resource e.g. Jar file
*
* @param p_file file relative to the CMain
* @return URL of file or null
*
* @throws URISyntaxException is thrown on URI errors
* @throws MalformedURLException is thrown on malformat
*/
@Nonnull
private static URL resourceurl( final File p_file ) throws URISyntaxException, MalformedURLException
{
if ( p_file.exists() )
return p_file.toURI().normalize().toURL();
final URL l_url = CCommon.class.getClassLoader().getResource( p_file.toString().replace( File.separator, "/" ) );
if ( Objects.isNull( l_url ) )
throw new CIllegalArgumentException( CCommon.languagestring( CCommon.class, "fileurlnull", p_file ) );
return l_url.toURI().normalize().toURL();
}
// --- language operations ---------------------------------------------------------------------------------------------------------------------------------
/**
* returns the language depend string on any object
*
* @param p_source any object
* @param p_label label name
* @param p_parameter parameter
* @return translated string
*
* @tparam T object type
*/
@Nonnull
public static <T> String languagestring( final T p_source, final String p_label, final Object... p_parameter )
{
return languagestring( p_source.getClass(), p_label, p_parameter );
}
/**
* returns a string of the resource file
*
* @param p_class class for static calls
* @param p_label label name of the object
* @param p_parameter object array with substitutions
* @return resource string
*/
@Nonnull
public static String languagestring( final Class<?> p_class, final String p_label, final Object... p_parameter )
{
try
{
return MessageFormat.format( LANGUAGE.getString( languagelabel( p_class, p_label ) ), p_parameter );
}
catch ( final MissingResourceException l_exception )
{
return "";
}
}
/**
* returns the label of a class and string to get access to the resource
*
* @param p_class class for static calls
* @param p_label label name of the object
* @return label name
*/
@Nonnull
private static String languagelabel( final Class<?> p_class, final String p_label )
{
return ( p_class.getCanonicalName().toLowerCase( Locale.ROOT ) + "." + p_label.toLowerCase( Locale.ROOT ) ).replaceAll( "[^a-zA-Z0-9_.]+", "" ).replace(
PACKAGEROOT + ".", "" );
}
// --- resource utf-8 encoding -----------------------------------------------------------------------------------------------------------------------------
/**
* class to read UTF-8 encoded property file
*
* @note Java default encoding for property files is ISO-Latin-1
*/
private static final class CUTF8Control extends ResourceBundle.Control
{
public final ResourceBundle newBundle( final String p_basename, final Locale p_locale, final String p_format, final ClassLoader p_loader,
final boolean p_reload
) throws IllegalAccessException, InstantiationException, IOException
{
final InputStream l_stream;
final String l_resource = this.toResourceName( this.toBundleName( p_basename, p_locale ), "properties" );
if ( !p_reload )
l_stream = p_loader.getResourceAsStream( l_resource );
else
{
final URL l_url = p_loader.getResource( l_resource );
if ( Objects.isNull( l_url ) )
return null;
final URLConnection l_connection = l_url.openConnection();
if ( Objects.isNull( l_connection ) )
return null;
l_connection.setUseCaches( false );
l_stream = l_connection.getInputStream();
}
final ResourceBundle l_bundle = new PropertyResourceBundle( new InputStreamReader( l_stream, "UTF-8" ) );
l_stream.close();
return l_bundle;
}
}
}