public class SdiDuplicatePersonException extends SdiSkipPersonException
{
    private static final long serialVersionUID = 1L;

    /**
     * Constructor
     *
     * @param aExitCode
     */
    public SdiDuplicatePersonException( Person<?> aPerson )
    {
        super( aPerson );
    }

    /**
     * Constructor
     *
     * @param aMessage
     * @param aExitCode
     */
    public SdiDuplicatePersonException( String aMessage, Person<?> aPerson )
    {
        super( aMessage, aPerson );
    }

} public class SdiException extends Exception
{
    private static final long serialVersionUID = 1L;

    public static final int EXIT_CODE_NO_ERROR = 0;
    public static final int EXIT_CODE_UNKNOWN_ERROR = 1;
    public static final int EXIT_CODE_PARSE_ERROR = 2;
    public static final int EXIT_CODE_CONFIG_ERROR = 3;
    public static final int EXIT_CODE_MAIL_ERROR = 4;
    public static final int EXIT_CODE_IMPORT_ERROR = 5;
    public static final int EXIT_CODE_FTP_ERROR = 6;
    public static final int EXIT_CODE_SSH_ERROR = 7;

    private int myExitCode;

    /**
     * Constructor
     *
     */
    public SdiException( int aExitCode )
    {
        super();
    }

    /**
     * Constructor
     *
     * @param aMessage
     */
    public SdiException( String aMessage, int aExitCode )
    {
        super( aMessage );
        myExitCode = aExitCode;
    }

    /**
     * Constructor
     *
     * @param aCause
     */
    public SdiException( Throwable aCause, int aExitCode )
    {
        super( aCause );
        myExitCode = aExitCode;
    }

    /**
     * Constructor
     *
     * @param aMessage
     * @param aCause
     */
    public SdiException( String aMessage, Throwable aCause, int aExitCode )
    {
        super( aMessage, aCause );
        myExitCode = aExitCode;
    }


    /**
     * @return exitCode
     */
    public int getExitCode()
    {
        return myExitCode;
    }

    /**
     * @param aThrowable
     * @return
     */
    public static SdiException toSdiException( Throwable aThrowable )
    {
        return toSdiException( aThrowable, SdiException.EXIT_CODE_UNKNOWN_ERROR );
    }

    /**
     * @param aThrowable
     * @return
     */
    public static SdiException toSdiException( Throwable aThrowable, int aErrorCode )
    {
        SdiException newEx;
        if ( aThrowable instanceof SdiException )
        {
            newEx = (SdiException) aThrowable;
        }
        else
        {
            newEx = new SdiException( "Throwable caught ", aThrowable, aErrorCode );
        } // if..else t instanceof SdiException

        return newEx;
    }

    @Override
    public String toString()
    {
        String result = super.toString();

        result += "; ExitCode: " + myExitCode;

        return result;
    }


} @Component
public class ConfigUtils
{

    /** logger for this class */
    private static Logger myLog = LogManager.getLogger( ConfigUtils.class );
    public static final String PROP_SOURCE_NAME_CMD_LINE = "CommandLineArguments";
    private static final String PROP_SOURCE_NAME_DYNAMIC = "DynamicProperties";
    public static final String KEY_PROP_OUTPUT_DIR = "dynamic.outputDir.file";

    // must be manually injected since static!
    private static ConversionService  myConversionService;

    /**
     * Converts the given classname into a properties file name according following rules:
     *
     * <ul>
     *     <li> if class is annotated with SdiProps the value of the annotation is used, suffixed by
     *          ".properties" (unless already present).</li>
     *     <li>if annotation is missing or no value configured:</li>
     *     <ul>
     *         <li> if class name ends with "Properties" this suffix will be truncated and replaced by
     *         ".properties"</li>
     *         <li> any other class name is used as is and suffixed with ".properties"</li>
     *     </ul>
     * </ul>
     *
     */
    public static String makePropertyResourceName( Class<?> aClass )
    {
        String found = null;

        SdiProps ann = aClass.getAnnotation( SdiProps.class );

        if ( ann != null )
        {
            found = ann.value();
        } // if ann != null

        if ( StringUtils.hasText( found ) )
        {
            if ( found.endsWith( ".properties" ) )
            {
                return found;
            } // if condition

            return found + ".properties";
        }

        String classname = aClass.getSimpleName();

        if ( classname.endsWith( "Properties" ) || classname.endsWith( "properties" ) )
        {
            return classname.substring( 0, classname.length() - "Properties".length() ) + ".properties";
        } // if condition

        return classname + ".properties";

    }

    /**
     * Tries to read the value from the given environment and to convert it to an int.
     * <p>
     * If the converstion fails, an excption is thrown.
     * <p>
     *
     * @param aEnv
     * @param aKey
     * @param aDefault
     * @return
     * @throws SdiException
     */
    public static int getIntProperty( Environment aEnv, String aKey ) throws SdiException
    {

        try
        {
            checkConversionServiceInitialized();
            return myConversionService.convert( aEnv.getRequiredProperty( aKey ), Integer.class );
        }
        catch ( Throwable t )
        {
            throw new SdiException( "No integer value found for property "+ aKey,
                                    t,
                                    SdiException.EXIT_CODE_CONFIG_ERROR );
        }
    }

    /**
     * @throws SdiException
     *
     */
    private static void checkConversionServiceInitialized() throws SdiException
    {
        if ( myConversionService == null )
        {
            myLog.warn( "ConversionService not initialized" );
            // Maybe forgotten in a unit test? Note: Without this check a forgotten injection in a unit
            // test would hardly be noticed if you get - e.g. - a int property with a default value
            throw new SdiException( "ConversionService not initialized", SdiException.EXIT_CODE_CONFIG_ERROR );
        } // if myConversionService == null

    }

    /**
     * Tries to read the value from the given environment and to convert it to an int.
     * <p>
     * If the converstion fails, the default value is returned.
     * <p>
     *
     * @param aEnv
     * @param aKey
     * @param aDefault
     * @return
     */
    public static int getIntProperty( Environment aEnv, String aKey, int aDefault )
    {
        try
        {
            checkConversionServiceInitialized();
            return myConversionService.convert( aEnv.getRequiredProperty( aKey ), Integer.class );
        }
        catch ( Throwable t )
        {
            return aDefault;
        }
    }

    /**
     * Tries to read the value from the given environment.
     * <p>
     * If the converstion fails, an excption is thrown.
     * <p>
     *
     * @param aEnv
     * @param aKey
     * @return
     * @throws SdiException
     */
    public static String getStringProperty( Environment aEnv,
                                            String aKey ) throws SdiException
    {
        try
        {
            return aEnv.getRequiredProperty( aKey );
        }
        catch ( Throwable t )
        {
            throw new SdiException( "No value found for property "+ aKey,
                                    t,
                                    SdiException.EXIT_CODE_CONFIG_ERROR );
        }
    }

    /**
     * Tries to read the value from the given environment and to convert it to a boolean.
     * <p>
     * If the conversion fails, an excption is thrown.
     * <p>
     *
     * @param aEnv
     * @param aKey
     * @return
     * @throws SdiException
     */
    public static boolean getBooleanProperty( Environment aEnv,
                                              String aKey ) throws SdiException
    {
        try
        {
            checkConversionServiceInitialized();
            return myConversionService.convert( aEnv.getRequiredProperty( aKey ), Boolean.class );
        }
        catch ( Throwable t )
        {
            throw new SdiException( "No boolean value found for property "+ aKey,
                                    SdiException.EXIT_CODE_CONFIG_ERROR );
        }
    }

    /**
     * Tries to read the value from the given environment and to convert it to a boolean.
     * <p>
     * If the converstion fails, the default value is returned.
     * <p>
     *
     * @param aEnv
     * @param aKey
     * @param aDefault
     * @return
     */
    public static boolean getBooleanProperty( Environment aEnv,
                                              String aKey,
                                              boolean aDefault )
    {
        try
        {
            return getBooleanProperty( aEnv, aKey );
        }
        catch ( Throwable t )
        {
            return aDefault;
        }
    }

    /**
     * Adds the given value to the environment, using the property source "DynamicProperties"
     * <p>
     * @param aEnv
     * @param aKey
     * @param aValue
     * @throws SdiException
     */
    public static void addToEnvironment( ConfigurableEnvironment aEnv,
                                         String aKey,
                                         Object aValue )
            throws SdiException
    {
        Map<String, Object> map = getOrCreatePropertySource( aEnv, PROP_SOURCE_NAME_DYNAMIC );

        myLog.debug( "setting property " + aKey + " = " + aValue + " into the environment" );
        map.remove( aKey );
        map.put( aKey, aValue );
    }

    /**
     * Removes the given key from the environment (property source "DynamicProperties")
     * <p>
     * @param aEnv
     * @param aKey
     * @throws SdiException
     */
    public static void removeFromEnvironment( ConfigurableEnvironment aEnv,
                                              String aKey )
            throws SdiException
    {
        Map<String, Object> map = getOrCreatePropertySource( aEnv, PROP_SOURCE_NAME_DYNAMIC );

        myLog.debug( "removing property " + aKey +  " from the environment" );
        map.remove( aKey );
    }

    /**
     * Tries to retrieve the named PropertySource from the environment. If not found it creates a
     * new one.
     * <p>
     *
     * @param aEnv
     * @param aPropertySourceName
     * @return the embedded property map
     * @throws SdiException if the found property source is not instance of PropertiesPropertySource
     */
    public static Map<String, Object> getOrCreatePropertySource( ConfigurableEnvironment aEnv,
                                                                 String aPropertySourceName ) throws SdiException
    {
        MutablePropertySources mps = aEnv.getPropertySources();
        PropertySource<?> ps = mps.get( aPropertySourceName );
        PropertiesPropertySource pps = null;

        if ( ps == null )
        {
            Properties props = new Properties();
            pps = new PropertiesPropertySource( aPropertySourceName, props );
            mps.addFirst( pps );
        }
        else
        {
            if ( !( ps instanceof PropertiesPropertySource ) )
            {
                throw new SdiException( "Found property source is not instance of PropertiesPropertySource "
                                        + " but: " + ps.getClass().getName(),
                                        SdiException.EXIT_CODE_CONFIG_ERROR );
            } // if !( ps instanceof PropertiesPropertySource )

            pps = (PropertiesPropertySource) ps;
        }

        Map<String,Object> map = pps.getSource();
        return map;
    }


    /**
     * Injects springs conversion service
     * <p>
     * @param  aConversionService
     *         myConversionService to set
     */
    public static void setConversionService( ConversionService aConversionService )
    {
        myConversionService = aConversionService;
    }

    /**
     * Assembles a set of property names starting with the given prefix.
     * <p>
     * The resulting set is sorted according the natural ordering of the full property name.
     * <p>
     * @param aEnv
     * @param aKeyPrefix
     * @return
     */
    public static Collection<String> getPropertyNamesStartingWith( ConfigurableEnvironment aEnv,
                                                                   String aKeyPrefix )
    {
        return getAllPropertyNames( aEnv ).stream()
            .filter( key -> key.startsWith( aKeyPrefix ) )
            .collect( Collectors.toCollection( (Supplier<Set<String>>) TreeSet::new ) );
    }

    /**
     * Returns a collection of all property names in the environment.
     * <p>
     * @param aEnv
     * @return
     */
    public static Collection<String> getAllPropertyNames( ConfigurableEnvironment aEnv )
    {
        Collection<String> result = new HashSet<>();
        aEnv.getPropertySources().forEach( ps -> result.addAll( getAllPropertyNames( ps ) ) );
        return result;
    }

    /**
     * Returns a collection of all property names in the given PropertySource.
     * <p>
     * @param aPropSource
     * @return
     */
    public static Collection<String> getAllPropertyNames( PropertySource<?> aPropSource )
    {
        Collection<String> result = new HashSet<>();

        if ( aPropSource instanceof CompositePropertySource)
        {
            CompositePropertySource cps = (CompositePropertySource) aPropSource;
            cps.getPropertySources().forEach( ps -> result.addAll( getAllPropertyNames( ps ) ) );
            return result;
        }

        if ( aPropSource instanceof EnumerablePropertySource<?> )
        {
            EnumerablePropertySource<?> ps = (EnumerablePropertySource<?>) aPropSource;
            Arrays.asList( ps.getPropertyNames() ).forEach( key -> result.add( key ) );
            return result;
        }

        // note: Most descendants of PropertySource are EnumerablePropertySource. There are some
        // few others like JndiPropertySource or StubPropertySource
        myLog.debug( "Given PropertySource is instanceof " + aPropSource.getClass().getName()
                     + " and cannot be iterated" );

        return result;
    }

}
 @SuppressWarnings( "rawtypes" )
abstract public class Person<T extends Map> extends EnumerablePropertySource<Map<String, Object>>
{

    /** logger for this class */
    private Logger myLog = LogManager.getLogger( Person.class );

    /**
     * @return username
     */
    public String getUsername()
    {
        return getStringProperty( PersonKey.THING_ALTERNATENAME.getKeyName() );
    }

    /**
     * @param  aUsername
     *         username to set
     */
    @SuppressWarnings( "unchecked" )
    public void setUsername( String aUsername )
    {
        ((Map) getSource() ).put( PersonKey.THING_ALTERNATENAME.getKeyName(), aUsername );
    }

    /**
     * @return givenname
     */
    public String getGivenname()
    {
        return getStringProperty( PersonKey.PERSON_GIVENNAME.getKeyName() );
    }

    /**
     * @param  aGivenname
     *         givenname to set
     */
    public void setGivenname( String aGivenname )
    {
        getSource().put( PersonKey.PERSON_GIVENNAME.getKeyName(), aGivenname );
    }

    /**
     * @return middlename
     */
    public String getMiddlename()
    {
        return getStringProperty( PersonKey.PERSON_ADDITIONALNAME.getKeyName() );
    }

    /**
     * @param  aMiddlename
     *         middlename to set
     */
    public void setMiddlename( String aMiddlename )
    {
        getSource().put( PersonKey.PERSON_ADDITIONALNAME.getKeyName(), aMiddlename );
    }

    /**
     * @return familyName
     */
    public String getFamilyName()
    {
        return getStringProperty( PersonKey.PERSON_FAMILYNAME.getKeyName() );
    }

    /**
     * @param  aFamilyName
     *         familyName to set
     */
    public void setFamilyName( String aFamilyName )
    {
        getSource().put( PersonKey.PERSON_FAMILYNAME.getKeyName(), aFamilyName );
    }

    /**
     * @return eMail
     */
    public String getEMail()
    {
        return getStringProperty( PersonKey.PERSON_EMAIL.getKeyName() );
    }

    /**
     * @param  aEMail
     *         eMail to set
     */
    public void setEMail( String aEMail )
    {
        getSource().put( PersonKey.PERSON_EMAIL.getKeyName(), aEMail );
    }

    /**
     * @return Gender
     */
    public String getGender()
    {
        return getStringProperty( PersonKey.PERSON_GENDER.getKeyName() );
    }

    /**
     * @param  aValue
     *         Gender
     */
    public void setGender( String aValue )
    {
        getSource().put( PersonKey.PERSON_BIRTHDATE.getKeyName(), aValue );
    }

    /**
     * @return Gender
     */
    public Date getBirthdate()
    {
        return getDateProperty( PersonKey.PERSON_BIRTHDATE.getKeyName() );
    }

    /**
     * @param  aValue
     *         Gender
     */
    public void setBirthdate( Date aValue )
    {
        getSource().put( PersonKey.PERSON_BIRTHDATE.getKeyName(), aValue );
    }

    /**
     * @see org.springframework.core.env.PropertySource#getProperty(java.lang.String)
     */
    @Override
    public Object getProperty( String aKey )
    {
        return getProperty( aKey, Object.class );
    }

    public String getStringProperty( String aKey )
    {
        return getProperty( aKey, String.class );
    }

    public Number getNumberProperty( String aKey )
    {
        return getProperty( aKey, Number.class );
    }

    public Date getDateProperty( String aKey )
    {
        return getProperty( aKey, Date.class );
    }

    public List<Long> getLongListProperty( String aKey )
    {
        List<Long> result = new ArrayList<Long>();
        List<?> list = getProperty( aKey, List.class );
        if ( list != null )
        {
            list.forEach( item -> result.add( (Long) item ) );
        } // if list != null
        return result;
    }


    @SuppressWarnings( "unchecked" )
    public <R> R getProperty( String aKey, Class<R> aClass )
    {
        Object o = getSource().get( aKey );

        if ( o instanceof NullField || o == null )
        {
            return null;
        } // if o instanceof NullField

        if ( aClass.isAssignableFrom( o.getClass() ) )
        {
            return (R) o;
        } // if aClass.isAssignableFrom( o.getClass() )

        throw new IllegalStateException( "Unexpected value type: " + o.getClass()
                                         + "; expected: " + aClass.getSimpleName() );
    }

    public void setProperty( String aKey, Object aValue )
    {
        getSource().put( aKey, aValue );
    }

    /**
     * @see org.springframework.core.env.EnumerablePropertySource#getPropertyNames()
     */
    @Override
    public String[] getPropertyNames()
    {
        Map<String, Object> map = getSource();
        return map.keySet().toArray( new String[0] );
    }

    @Override
    public boolean containsProperty( String aKey )
    {
        Map<String, Object> map = getSource();
        return map.keySet().contains( aKey );
    }

    /**
     * Constructor
     *
     * @param aName
     * @param aSource
     */
    @SuppressWarnings( "unchecked" )
    public <F extends Map> Person( String aName, F aSource )
    {
        super( aName, aSource );
    }


}
 public enum PersonKey
{
    /**
     * Description: An additional name for a Person, can be used for a middle name.<p>
     * Type       : Text<p>
     */
    PERSON_ADDITIONALNAME ( "person.additionalName" ),

    /**
     * Description: Physical address of the item.<p>
     * Type       : PostalAddress<p>
     */
    PERSON_ADDRESS ( "person.address" ),

    /**
     * Description: An organization that this person is affiliated with. For example, a school/university,
     *              a club, or a team.<p>
     * Type       : Organization<p>
     */
    PERSON_AFFILIATION ( "person.affiliation" ),

    /**
     * Description: An educational organizations that the person is an alumni of.
     *              Inverse property: alumni.<p>
     * Type       : EducationalOrganization<p>
     */
    PERSON_ALUMNIOF ( "person.alumniOf" ),

    /**
     * Description: An award won by this person or for this creative work. Supersedes awards.<p>
     * Type       : Text<p>
     */
    PERSON_AWARD ( "person.award" ),

    /**
     * Description: Date of birth.<p>
     * Type       : Date<p>
     */
    PERSON_BIRTHDATE ( "person.birthDate" ),

    /**
     * Description: The brand(s) associated with a product or service, or the brand(s) maintained by an
     *              organization or business person.<p>
     * Type       : Organization,Brand<p>
     */
    PERSON_BRAND ( "person.brand" ),

    /**
     * Description: A child of the person.<p>
     * Type       : Person<p>
     */
    PERSON_CHILDREN ( "person.children" ),

    /**
     * Description: A colleague of the person. Supersedes colleagues.<p>
     * Type       : Person<p>
     */
    PERSON_COLLEAGUE ( "person.colleague" ),

    /**
     * Description: A contact point for a person or organization. Supersedes contactPoints.<p>
     * Type       : ContactPoint<p>
     */
    PERSON_CONTACTPOINT ( "person.contactPoint" ),

    /**
     * Description: Date of death.<p>
     * Type       : Date<p>
     */
    PERSON_DEATHDATE ( "person.deathDate" ),

    /**
     * Description: The Dun & Bradstreet DUNS number for identifying an organization or business person.<p>
     * Type       : Text<p>
     */
    PERSON_DUNS ( "person.duns" ),

    /**
     * Description: Email address.<p>
     * Type       : Text<p>
     */
    PERSON_EMAIL ( "person.email" ),

    /**
     * Description: Family name. In the U.S., the last name of an Person. This can be used along with
     *              givenName instead of the Name property.<p>
     * Type       : Text<p>
     */
    PERSON_FAMILYNAME ( "person.familyName" ),

    /**
     * Description: The fax number.<p>
     * Type       : Text<p>
     */
    PERSON_FAXNUMBER ( "person.faxNumber" ),

    /**
     * Description: The most generic uni-directional social relation.<p>
     * Type       : Person<p>
     */
    PERSON_FOLLOWS ( "person.follows" ),

    /**
     * Description: Gender of the person.<p>
     * Type       : Text<p>
     */
    PERSON_GENDER ( "person.gender" ),

    /**
     * Description: Given name. In the U.S., the first name of a Person. This can be used along with
     *              familyName instead of the Name property.<p>
     * Type       : Text<p>
     */
    PERSON_GIVENNAME ( "person.givenName" ),

    /**
     * Description: The Global Location Number (GLN, sometimes also referred to as International Location
     *              Number or ILN) of the respective organization, person, or place. The GLN is a 13-digit
     *              number used to identify parties and physical locations.<p>
     * Type       : Text<p>
     */
    PERSON_GLOBALLOCATIONNUMBER ( "person.globalLocationNumber" ),

    /**
     * Description: Points-of-Sales operated by the organization or person.<p>
     * Type       : Place<p>
     */
    PERSON_HASPOS ( "person.hasPOS" ),

    /**
     * Description: A contact location for a person's residence.<p>
     * Type       : ContactPoint,Place<p>
     */
    PERSON_HOMELOCATION ( "person.homeLocation" ),

    /**
     * Description: An honorific prefix preceding a Person's name such as Dr/Mrs/Mr.<p>
     * Type       : Text<p>
     */
    PERSON_HONORIFICPREFIX ( "person.honorificPrefix" ),

    /**
     * Description: An honorific suffix preceding a Person's name such as M.D. /PhD/MSCSW.<p>
     * Type       : Text<p>
     */
    PERSON_HONORIFICSUFFIX ( "person.honorificSuffix" ),

    /**
     * Description: A count of a specific user interactions with this item, for example, 20 UserLikes,
     *              5 UserComments, or 300 UserDownloads. The user interaction type should be one of the
     *              sub types of UserInteraction.<p>
     * Type       : Text<p>
     */
    PERSON_INTERACTIONCOUNT ( "person.interactionCount" ),

    /**
     * Description: The International Standard of Industrial Classification of All Economic Activities
     *              (ISIC), Revision 4 code for a particular organization, business person, or place.<p>
     * Type       : Text<p>
     */
    PERSON_ISICV4 ( "person.isicV4" ),

    /**
     * Description: The job title of the person (for example, Financial Manager).<p>
     * Type       : Text<p>
     */
    PERSON_JOBTITLE ( "person.jobTitle" ),

    /**
     * Description: The most generic bi-directional social/work relation.<p>
     * Type       : Person<p>
     */
    PERSON_KNOWS ( "person.knows" ),

    /**
     * Description: A pointer to products or services offered by the organization or person.<p>
     * Type       : Offer<p>
     */
    PERSON_MAKESOFFER ( "person.makesOffer" ),

    /**
     * Description: An Organization (or ProgramMembership) to which this Person or Organization belongs.
     *              Inverse property: member.<p>
     * Type       : ProgramMembership,Organization<p>
     */
    PERSON_MEMBEROF ( "person.memberOf" ),

    /**
     * Description: The North American Industry Classification System (NAICS) code for a particular
     *              organization or business person.<p>
     * Type       : Text<p>
     */
    PERSON_NAICS ( "person.naics" ),

    /**
     * Description: Nationality of the person.<p>
     * Type       : Country<p>
     */
    PERSON_NATIONALITY ( "person.nationality" ),

    /**
     * Description: Products owned by the organization or person.<p>
     * Type       : OwnershipInfo,Product<p>
     */
    PERSON_OWNS ( "person.owns" ),

    /**
     * Description: A parent of this person. Supersedes parents.<p>
     * Type       : Person<p>
     */
    PERSON_PARENT ( "person.parent" ),

    /**
     * Description: Event that this person is a performer or participant in.<p>
     * Type       : Event<p>
     */
    PERSON_PERFORMERIN ( "person.performerIn" ),

    /**
     * Description: The most generic familial relation.<p>
     * Type       : Person<p>
     */
    PERSON_RELATEDTO ( "person.relatedTo" ),

    /**
     * Description: A pointer to products or services sought by the organization or person (demand).<p>
     * Type       : Demand<p>
     */
    PERSON_SEEKS ( "person.seeks" ),

    /**
     * Description: A sibling of the person. Supersedes siblings.<p>
     * Type       : Person<p>
     */
    PERSON_SIBLING ( "person.sibling" ),

    /**
     * Description: The person's spouse.<p>
     * Type       : Person<p>
     */
    PERSON_SPOUSE ( "person.spouse" ),

    /**
     * Description: The Tax / Fiscal ID of the organization or person, e.g. the TIN in the US or the
     *              CIF/NIF in Spain.<p>
     * Type       : Text<p>
     */
    PERSON_TAXID ( "person.taxID" ),

    /**
     * Description: The telephone number.<p>
     * Type       : Text<p>
     */
    PERSON_TELEPHONE ( "person.telephone" ),

    /**
     * Description: The Value-added Tax ID of the organization or person.<p>
     * Type       : Text<p>
     */
    PERSON_VATID ( "person.vatID" ),

    /**
     * Description: A contact location for a person's place of work.<p>
     * Type       : ContactPoint,Place<p>
     */
    PERSON_WORKLOCATION ( "person.workLocation" ),

    /**
     * Description: Organizations that the person works for.<p>
     * Type       : Organization<p>
     */
    PERSON_WORKSFOR ( "person.worksFor" ),

    //////////////////////////////////////////////////////////////////
    // inherited Properties from Thing
    //////////////////////////////////////////////////////////////////

    /**
     * Description: An additional type for the item, typically used for adding more specific types from
     *              external vocabularies in microdata syntax. This is a relationship between something
     *              and a class that the thing is in. In RDFa syntax, it is better to use the native RDFa
     *              syntax - the 'typeof' attribute - for multiple types. Schema.org tools may have only
     *              weaker understanding of extra types, in particular those defined externally.<p>
     * Type       : URL<p>
     */
    THING_ADDITIONALTYPE ( "thing.additionalType" ),

    /**
     * Description: An alias for the item. Here used for username or loginname<p>
     * Type       : Text<p>
     */
    THING_ALTERNATENAME ( "thing.alternateName" ),

    /**
     * Description: A short description of the item.<p>
     * Type       : Text<p>
     */
    THING_DESCRIPTION ( "thing.description" ),

    /**
     * Description: An image of the item. This can be a URL or a fully described ImageObject.<p>
     * Type       : URL,ImageObject<p>
     */
    THING_IMAGE ( "thing.image" ),

    /**
     * Description: The name of the item.<p>
     * Type       : Text<p>
     */
    THING_NAME ( "thing.name" ),

    /**
     * Description: Indicates a potential Action, which describes an idealized action in which this thing
     *              would play an 'object' role.<p>
     * Type       : Action<p>
     */
    THING_POTENTIALACTION ( "thing.potentialAction" ),

    /**
     * Description: URL of a reference Web page that unambiguously indicates the item's identity. E.g.
     *              the URL of the item's Wikipedia page, Freebase page, or official website.<p>
     * Type       : URL<p>
     */
    THING_SAMEAS ( "thing.sameAs" ),

    /**
     * Description: URL of the item.<p>
     * Type       : URL<p>
     */
    THING_URL ( "thing.url" )
    ;

    private String myKeyName;

    /**
     * Constructor
     *
     * @param aKeyName
     */
    private PersonKey( String aKeyName )
    {
        myKeyName = aKeyName;
    }

    /**
     * @return keyName
     */
    public String getKeyName()
    {
        return myKeyName;
    }

    public static final String KEY_CUSTOM_KEYS = "sdi.person.customkeys";
    private static List<String> myCustomKeys = new ArrayList<String>();

    /**
     * @param aEnv
     */
    public static void initCustomKeys( ConfigurableEnvironment aEnv )
    {
        myCustomKeys = new ArrayList<String>();

        String configured = aEnv.getProperty( KEY_CUSTOM_KEYS );
        if ( !StringUtils.hasText( configured ) )
        {
            return;
        } // if !StringUtils.hasText( configured )

        String[] keys = configured.trim().split( "," );

        for ( String key : keys )
        {
            myCustomKeys.add( key );
        }
    }

    public static List<String> getKeyNames()
    {
        List<String> result = new ArrayList<String>();

        for ( PersonKey key : PersonKey.values() )
        {
            result.add( key.getKeyName() );
        }

        result.addAll( myCustomKeys );

        return result;
    }




} public abstract class ConverterImage implements FieldConverter<BufferedImage>
{

    /**
     * Converts the content of the given input stream to a BufferedImage.
     * @param aInputStream
     * @return
     * @throws SdiException
     */
    public BufferedImage convert( InputStream aInputStream ) throws SdiException
    {
        try
        {
            return ImageIO.read( aInputStream );
        }
        catch ( IOException e )
        {
            throw new SdiException( "Problems reading image input stream", e, SdiException.EXIT_CODE_PARSE_ERROR );
        }
    }

    /**
     * Resizes the given image to the given width and height.
     *
     * @param originalImage
     * @param aWidth
     * @param aHeight
     * @return
     */
    public static BufferedImage resizeImage( BufferedImage originalImage,
                                             int aWidth,
                                             int aHeight )
    {
        int type = originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType();

        BufferedImage resizedImage = new BufferedImage( aWidth, aHeight, type );
        Graphics2D g = resizedImage.createGraphics();
        g.setComposite( AlphaComposite.Src );
        g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR );
        g.setRenderingHint( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY );
        g.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
        g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
        g.drawImage( originalImage, 0, 0, aWidth, aHeight, null );
        g.dispose();

        return resizedImage;
    }
} @Component
public class MailJobDefault implements MailJob
{
    /** logger for this class */
    private Logger myLog = LogManager.getLogger( MailJobDefault.class );
    @Autowired
    private Environment myEnv;
    @Autowired
    private MailCreator myMailCreator;
    @Autowired
    private MailSenderDefault myMailSender;

    /**
     * Constructor
     *
     */
    public MailJobDefault()
    {
        super();
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#execute(ch.sdi.core.impl.data.Person)
     */
    @Override
    public void execute( Person<?> aPerson ) throws SdiException
    {
        Email email = myMailCreator.createMailFor( aPerson );
        myMailSender.sendMail( email );

        ReportMsg msg = new ReportMsg( ReportType.MAIL_TARGET,
                                       aPerson.getEMail(),
                                       new MailWrap( email ) );
        myLog.info( msg );
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#init()
     */
    @Override
    public void init() throws SdiException
    {
        myMailCreator.init();
        myMailSender.init();
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#close()
     */
    @Override
    public void close() throws SdiException
    {
        myLog.debug( "closing the mail job" );
    }

    class MailWrap
    {
        Email myMail;

        /**
         * Constructor
         *
         * @param aMail
         */
        public MailWrap( Email aMail )
        {
            super();
            myMail = aMail;
        }

        @Override
        public String toString()
        {
            StringBuilder sb = new StringBuilder( super.toString() );
            sb.append( "\n    Receiver     : " ).append( myMail.getToAddresses() );
            sb.append( "\n    Subject : " ).append( myMail.getSubject() );
            return sb.toString();

        }
    }
} @Component
public class MailTextResolver
{

    /** logger for this class */
    private Logger myLog = LogManager.getLogger( MailTextResolver.class );

    private static final String KEY_PERSON_TEMP = "tempPersonInEnv";
    private static final String KEY_BODY_BY_TEMPLATE_FILE = "dyn.mail.body.bytemplatefile";
    public static final String KEY_EXTRA_MAIL_BODY_FILE = "person.extraMailBody.filename";
    public static final String KEY_EXTRA_MAIL_BODY_TEXT = "person.extraMailBody.text";


    @Autowired
    private ConfigurableEnvironment myEnv;

    private String myBodyKey;

    /**
     * Initializes the mail content configuration and provides the resolved body in the environment with
     * the key dyn.mail.body.bytemplatefile
     * <p>
     * A configured body template file (sdi.mail.body.template) takes precedence over the key
     * sdi.mail.body
     * <p>
     * @throws SdiException on any problem
     */
    public void init() throws SdiException
    {
        myBodyKey = MailProperties.KEY_BODY;

        if ( myEnv.containsProperty( MailProperties.KEY_BODY_TEMPLATE ) )
        {
            String charsetName;

            try
            {
                charsetName = myEnv.getRequiredProperty( MailProperties.KEY_BODY_TEMPLATE_CHARSET );
            }
            catch ( Throwable t )
            {
                throw new SdiException( "Property " + MailProperties.KEY_BODY_TEMPLATE + " is defined but "
                                        + "not " + MailProperties.KEY_BODY_TEMPLATE_CHARSET,
                                        t,
                                        SdiException.EXIT_CODE_CONFIG_ERROR );
            }

            Charset charset;

            try
            {
                charset = Charset.forName( charsetName );
            }
            catch ( Throwable t )
            {
                throw new SdiException( "Property " + MailProperties.KEY_BODY_TEMPLATE_CHARSET
                                        + " (" + charsetName + ") is invalid",
                                        t,
                                        SdiException.EXIT_CODE_CONFIG_ERROR );
            }

            String fileName = myEnv.getProperty( MailProperties.KEY_BODY_TEMPLATE );
            String body = loadFile( charset, fileName );
            myBodyKey = KEY_BODY_BY_TEMPLATE_FILE;
            ConfigUtils.addToEnvironment( myEnv, myBodyKey, body );

        } // if myEnv.containsProperty( MailProperties.BODY_TEMPLATE )
    }

    /**
     * @param aCharset
     * @param aFileName
     * @return
     * @throws SdiException
     */
    private String loadFile( Charset aCharset, String aFileName ) throws SdiException
    {
        File file = new File( aFileName );

        String body;
        try
        {
            myLog.debug( "Loading mail body from template file " + file.getCanonicalPath() );

            InputStream is = new FileInputStream( file );

            try
            {
                body = StreamUtils.copyToString( is, aCharset );
            }
            finally
            {
                is.close();
            }
            myLog.debug( "Loaded mail body from template file " + body );
        }
        catch ( Throwable t )
        {
            throw new SdiException( "Cannot load mail template " + aFileName,
                                    t,
                                    SdiException.EXIT_CODE_CONFIG_ERROR );
        }
        return body;
    }

    public String getResolvedBody( Person<?> aPerson ) throws SdiException
    {
        if ( aPerson.containsProperty( KEY_EXTRA_MAIL_BODY_FILE ) )
        {
            String key = KEY_EXTRA_MAIL_BODY_TEXT;

            try
            {
                String fileName = aPerson.getStringProperty( KEY_EXTRA_MAIL_BODY_FILE );
                String body = loadFile( Charset.forName( "UTF-8" ), fileName );
                ConfigUtils.addToEnvironment( myEnv, key, body );
                return resolveValue( aPerson, key );
            }
            finally
            {
                ConfigUtils.removeFromEnvironment( myEnv, key );
            }
        }

        return resolveValue( aPerson, myBodyKey );

    }

    public String getResolvedSubject( Person<?> aPerson ) throws SdiException
    {
        return resolveValue( aPerson, MailProperties.KEY_SUBJECT );
    }

    /**
     * @param aPerson
     * @param aValue
     * @return
     * @throws SdiException
     */
    private String resolveValue( Person<?> aPerson, String aKey ) throws SdiException
    {
        myLog.debug( "Resolving value for key " + aKey );
        MapPropertySource mps = new MapPropertySource( KEY_PERSON_TEMP, aPerson.getSource() );
        myEnv.getPropertySources().addFirst( mps );
        try
        {
            return myEnv.getRequiredProperty( aKey );
        }
        catch ( Throwable t )
        {
            throw new SdiException( "Problems loading property " + aKey + ". Original exception message:\n"
                                    + t.getMessage(),
                                    t,
                                    SdiException.EXIT_CODE_CONFIG_ERROR );

        }
        finally
        {
            myEnv.getPropertySources().remove( KEY_PERSON_TEMP );
        }
    }

} public interface CustomTargetJobContext extends TargetJobContext
{

} public interface FtpJob extends TargetJob
{

} public interface PasswordEncryptor
{

    /**
     * Encrypts (or hashes) the given password
     * <p>
     *
     * @param aPassword
     * @return the encrypted password
     * @throws NoSuchAlgorithmException
     */
    public String encrypt( String aPassword );
}
 public interface TargetJob
{

    /**
     * Initializes the job. Is invoked once before all persons will be executed. Resources which are
     * valid for all persons can be allocated here.
     * <p>
     * @throws SdiException on any problem
     */
    public void init() throws SdiException;

    /**
     * Closes the job. Is invoked once after all persons have been executed. Resources can be freed.
     * <p>
     * @throws SdiException on any problem
     */
    public void close() throws SdiException;

    /**
     * Executes the concrete job for the given person
     * <p>
     *
     * @param aPerson
     *        must not be <code>null</code>
     * @throws SdiException
     *         on any problem
     */
    void execute( Person<?> aPerson ) throws SdiException;

} @Component
public class OxSqlJob implements SqlJob
{
    /** logger for this class */
    private Logger myLog = LogManager.getLogger( OxSqlJob.class );
    public static final String KEY_PREFIX_PROFILE_QUESTION = "ox.target.qn.";
    public static final String KEY_DEFAULT_GROUPS = "ox.target.defaultGroups";
    public static final String KEY_DEFAULT_ROLES = "ox.target.defaultRoles";
    public static final String KEY_GROUP_PRIVACY = "ox.target.groups.privacy";


    @Autowired
    private Environment myEnv;
    @Autowired
    private OxQuestionFactory myQuestionFactory;

    // does not work: @PersistenceContext(unitName="oxwall")
    protected  HibernateEntityManager myEntityManager;
    private boolean myDryRun;
    private boolean myCheckDuplicateOnDryRun;
    private long myDummyId = 1;
    private List<OxProfileQuestion> myProfileQuestions;
    private List<Long> myDefaultGroups;
    private List<Long> myDefaultRoles;
    private String myGroupPrivacy;
    private String myUserAccountType;
    private Integer myJoinIp;
    private boolean myEmailVerify;

    /**
     * Constructor
     *
     */
    public OxSqlJob()
    {
        super();
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#execute(ch.sdi.core.impl.data.Person)
     */
    @Override
    public void execute( Person<?> aPerson ) throws SdiException
    {
        List<Object> dbEntities = new ArrayList<Object>();

        if ( aPerson.containsProperty( OxTargetJobContext.KEY_NEEDS_ACTIVATION ) )
        {
            OxUser user = findPersonByEmail( aPerson.getEMail() );

            if ( user == null )
            {
                throw new SdiException( "person has property " + OxTargetJobContext.KEY_NEEDS_ACTIVATION
                                        + " but we cannot find the user entity by email address",
                                        SdiException.EXIT_CODE_UNKNOWN_ERROR );
            }

            String msg;

            if ( myDryRun )
            {
                msg = "Skip deleting the approve flag, since dryrun is true";
            }
            else
            {
                msg = "Deleting entry in ow_base_user_disapprove for user " + user.getId();
                OxUserUnapproved userUnapproved = findUnapprovedUser( user.getId() );
                myEntityManager.remove( userUnapproved );
            }

            myLog.info( msg );
            return;
        }

        myLog.debug( "creating new user entity" );
        OxUser user = new OxUser();
        user.setUsername( aPerson.getStringProperty( PersonKey.THING_ALTERNATENAME.getKeyName() ) );
        user.setAccountType( myUserAccountType );
        user.setActivityStamp( OxUtils.dateToLong( new Date() ) );
        user.setEmail( aPerson.getEMail() );
        user.setJoinIp( myJoinIp.longValue() );
        user.setJoinStamp( OxUtils.dateToLong( new Date() ) );
        user.setPassword( aPerson.getStringProperty( OxTargetJobContext.KEY_ENCRYPTED_PASSWORD ));
        user.setEmailVerify( myEmailVerify );
        saveEntity( dbEntities, user );

        aPerson.setProperty( OxTargetJobContext.KEY_PERSON_USER_ID, user.getId() );;

        List<Long> roles = resolveRoles( aPerson );
        if ( roles.size() > 0 )
        {
            myLog.debug( "creating group membership entities" );
            for ( Long role : roles )
            {
                OxUserRole roleEntity = new OxUserRole();
                roleEntity.setUserId( user.getId() );
                roleEntity.setRoleId( role );
                saveEntity( dbEntities, roleEntity );
            }
        } // if groups.size() > 0

        myLog.debug( "creating profile question entities" );
        for ( OxProfileQuestion question : myProfileQuestions )
        {
            if ( !question.hasValue( aPerson ) )
            {
                continue;
            } // if !question.hasValue( aPerson )

            OxProfileData profileData = new OxProfileData();
            profileData.setUserId( user.getId() );
            question.fillValues( profileData, aPerson );
            saveEntity( dbEntities, profileData );
        }

        Long avatarHash = aPerson.getProperty( OxTargetJobContext.KEY_AVATAR_HASH, Long.class );
        if ( avatarHash != null )
        {
            myLog.debug( "creating avatar entities" );
            OxAvatar avatar = new OxAvatar();
            avatar.setUserId( user.getId() );
            avatar.setHash( avatarHash );
            saveEntity( dbEntities, avatar );
        } // if StringUtils.hasText( avatarHash )

        List<Long> groups = resolveMembership( aPerson );
        if ( groups.size() > 0 )
        {
            myLog.debug( "creating group membership entities" );
            for ( Long group : groups )
            {
                OxUserGroupMembership groupEntity = new OxUserGroupMembership();
                groupEntity.setGroupId( group );
                groupEntity.setUserId( user.getId() );
                groupEntity.setTimeStamp( OxUtils.dateToLong( new Date() ) );
                groupEntity.setPrivacy( myGroupPrivacy );
                saveEntity( dbEntities, groupEntity );
            }
        } // if groups.size() > 0

        ReportMsg msg = new ReportMsg( ReportType.SQL_TARGET,
                                       aPerson.getEMail(),
                                       dbEntities.toArray() );
        myLog.info( msg );
    }

    /**
     * @param aPerson
     * @return
     */
    private List<Long> resolveRoles( Person<?> aPerson )
    {
        List<Long> result = new ArrayList<Long>();  // TODO: add roles per user, as with group membership
        myLog.debug( "collected roles of person: " + result );
        result.addAll( myDefaultRoles );
        myLog.debug( "resolved roles of person: " + result );
        return result;
    }

    /**
     * @param aPerson
     * @return
     * @throws SdiException
     */
    private List<Long> resolveMembership( Person<?> aPerson ) throws SdiException
    {
        List<Long> result = aPerson.getLongListProperty( PersonKey.PERSON_MEMBEROF.getKeyName() );
        myLog.debug( "collected groups of person: " + result );
        result.addAll( myDefaultGroups );
        myLog.debug( "resolved groups of person: " + result );
        return result;
    }

    /**
     * @param dbEntities
     * @param aEntity
     * @throws SdiException
     */
    private void saveEntity( List<Object> dbEntities, Object aEntity ) throws SdiException
    {
        if ( myDryRun )
        {
            myLog.trace( "DryRun: Going to save entity: " + aEntity );

            try
            {
                MethodUtils.invokeExactMethod( aEntity, "setId", new Object[] { Long.valueOf( myDummyId ) } );
            }
            catch ( Throwable t )
            {
                throw new SdiException( "Entity has no 'setId( Long )' method", t, SdiException.EXIT_CODE_UNKNOWN_ERROR );
            }

            myDummyId++;
        }
        else
        {
            myLog.trace( "Going to save entity: " + aEntity );
            myEntityManager.persist( aEntity );
        } // if..else myDryRun

        dbEntities.add( aEntity );
    }

    /**
     * @see ch.sdi.core.intf.SqlJob#isAlreadyPresent(ch.sdi.core.impl.data.Person)
     */
    @Override
    public boolean isAlreadyPresent( Person<?> aPerson ) throws SdiException
    {
        if ( myDryRun && !myCheckDuplicateOnDryRun )
        {
            myLog.debug( "DryRun is active. Not checking for duplicate person" );
            return false;
        } // if myDryRun

        OxUser oxUser = findPersonByEmail( aPerson.getEMail() );

        if ( oxUser != null )
        {
            myLog.debug( "given Person is already present: " + oxUser );
            return true;
        } // if results.size() > 0

        return false;
    }

    /**
     * @param aEmail
     * @return
     */
    public OxUser findPersonByEmail( String aEmail )
    {
        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();

        CriteriaQuery<OxUser> criteria = cb.createQuery(OxUser.class);
        Root<OxUser> root = criteria.from(OxUser.class);
        ParameterExpression<String> mailParam = cb.parameter(String.class);
        criteria.select(root).where(cb.equal( root.get("email"), mailParam ));

        TypedQuery<OxUser> queryEMail = myEntityManager.createQuery(criteria);

        queryEMail.setParameter( mailParam, aEmail );
        List<OxUser> results = queryEMail.getResultList();

        OxUser oxUser = null;

        if ( results.size() > 0 )
        {
            oxUser = results.get( 0 );
        } // if results.size() > 0

        return oxUser;
    }

    /**
     * @param aUserId
     * @return
     */
    public OxUserUnapproved findUnapprovedUser( Long aUserId )
    {
        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();

        CriteriaQuery<OxUserUnapproved> criteria = cb.createQuery(OxUserUnapproved.class);
        Root<OxUserUnapproved> root = criteria.from(OxUserUnapproved.class);
        ParameterExpression<Long> param = cb.parameter(Long.class);
        criteria.select(root).where(cb.equal( root.get("userId"), param ));

        TypedQuery<OxUserUnapproved> queryEMail = myEntityManager.createQuery(criteria);

        queryEMail.setParameter( param, aUserId );
        List<OxUserUnapproved> results = queryEMail.getResultList();

        OxUserUnapproved userUnapproved = null;

        if ( results.size() > 0 )
        {
            userUnapproved = results.get( 0 );
        } // if results.size() > 0

        return userUnapproved;
    }

    /**
     * Checks if the given hash is present in the ow_base_avatar table
     *
     * @param aHash
     * @return
     */
    public boolean isAvatarHashPresent( Long aHash )
    {
        if ( myDryRun )
        {
            myLog.debug( "DryRun is active. Not checking for duplicate avatar hash" );
            return false;
        } // if myDryRun

        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();

        CriteriaQuery<OxAvatar> criteria = cb.createQuery(OxAvatar.class);
        Root<OxAvatar> root = criteria.from(OxAvatar.class);
        ParameterExpression<Long> avatarParam = cb.parameter(Long.class);
        avatarParam = cb.parameter(Long.class);
        criteria.select(root).where(cb.equal( root.get("hash"), avatarParam ));
        TypedQuery<OxAvatar> query = myEntityManager.createQuery(criteria);
        query.setParameter( avatarParam, aHash );
        List<OxAvatar> results = query.getResultList();

        if ( results.size() > 0 )
        {
            myLog.debug( "given avatar hash is already present: " + aHash );
            return true;
        } // if results.size() > 0

        return false;
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#init()
     */
    @Override
    public void init() throws SdiException
    {
        myDryRun = ConfigUtils.getBooleanProperty( myEnv, SdiMainProperties.KEY_DRYRUN, false );
        myCheckDuplicateOnDryRun = ConfigUtils.getBooleanProperty( myEnv,
                                                                   SdiMainProperties.KEY_CHECK_DUPLICATE_ON_DRYRUN,
                                                                   false );
        myGroupPrivacy = myEnv.getProperty( KEY_GROUP_PRIVACY, "everybody" );
        myUserAccountType = myEnv.getProperty( OxTargetConfiguration.KEY_USER_ACCOUNT_TYPE );
        myJoinIp = ConfigUtils.getIntProperty( myEnv, OxTargetConfiguration.KEY_USER_JOINIP, 12345 );
        myEmailVerify = ConfigUtils.getBooleanProperty( myEnv, OxTargetConfiguration.KEY_USER_EMAIL_VERIFY );


        myEntityManager = EntityManagerProvider.getEntityManager( "oxwall" );

        if ( myEntityManager == null )
        {
            throw new SdiException( "Problems initializing EntityManager",
                                    SdiException.EXIT_CODE_CONFIG_ERROR );
        } // if em == null

        initProfileQuestions();
        initDefaultGroups();
        initDefaultRoles();

    }

    /**
     *
     */
    private void initDefaultRoles() throws SdiException
    {
        String configured = myEnv.getProperty( KEY_DEFAULT_ROLES, "" );
        myDefaultRoles = ConverterNumberList.toLongList( configured, "," );
        myLog.debug( "Configured default roles: " + myDefaultRoles );

    }

    /**
     * @throws SdiException
     */
    private void initDefaultGroups() throws SdiException
    {
        String configured = myEnv.getProperty( KEY_DEFAULT_GROUPS, "" );
        myDefaultGroups = ConverterNumberList.toLongList( configured, "," );
        myLog.debug( "Configured default groups: " + myDefaultGroups );
    }

    /**
     * Checks the configuration if there are profile questions configured and populates the
     * myProfileQuestions member with the found configurations.
     * <p>
     * @throws SdiException
     */
    private void initProfileQuestions() throws SdiException
    {
        myProfileQuestions = new ArrayList<OxProfileQuestion>();

        for ( String personKey : PersonKey.getKeyNames() )
        {
            String key = KEY_PREFIX_PROFILE_QUESTION + personKey;
            myLog.trace( "Looking up profile question configuration " + key );
            String configured = myEnv.getProperty( key );

            if ( !StringUtils.hasText( configured ) )
            {
                continue;
            } // if !StringUtils.hasText( configured )

            myLog.trace( "Found profile question configuration " + configured );

            String[] values = configured.trim().split( ":" );
            if ( values.length < 2 )
            {
                throw new SdiException( "Profile question not configured correctly: " + configured,
                                        SdiException.EXIT_CODE_CONFIG_ERROR );
            }

            OxQuestionType type;
            try
            {
                type = OxQuestionType.valueOf( values[0] );
            }
            catch ( IllegalArgumentException t )
            {
                throw new SdiException( "Profile question not configured correctly: " + configured + "; "
                                        + "type cannot be resolved",
                                        SdiException.EXIT_CODE_CONFIG_ERROR );
            }

            OxProfileQuestion question = null;

            switch ( type )
            {
                case text:
                    question = new OxProfileQuestionString( values[1], personKey );
                    break;
                case number:
                    question = new OxProfileQuestionNumber( values[1], personKey );
                    break;
                case date:
                    question = new OxProfileQuestionDate( values[1], personKey );
                    break;
                case custom:
                    if ( values.length < 3 )
                    {
                        throw new SdiException( "Profile question not configured correctly: " + values,
                                                SdiException.EXIT_CODE_CONFIG_ERROR );
                    }

                    question = myQuestionFactory.getCustomQuestion( values[1], values[2], personKey );
                    break;
                default:
                    throw new SdiException( "unhandled question type: " + type,
                                            SdiException.EXIT_CODE_UNKNOWN_ERROR );
            }

            question.init( myEnv );
            myLog.debug( "Adding profile question for key '" + personKey + "': " + question );
            myProfileQuestions.add( question );

        }
    }

    /**
     * @see ch.sdi.core.intf.TargetJob#close()
     */
    @Override
    public void close() throws SdiException
    {
        if ( myEntityManager != null )
        {
            myEntityManager.close();
        } // if em != null
    }

    /**
     * @see ch.sdi.core.intf.SqlJob#startTransaction()
     */
    @Override
    public void startTransaction()
    {
        myEntityManager.getTransaction().begin();
    }

    /**
     * @see ch.sdi.core.intf.SqlJob#commitTransaction()
     */
    @Override
    public void commitTransaction()
    {
        if ( myEntityManager != null )
        {
            if ( myEntityManager.getTransaction().isActive() )
            {
                myEntityManager.getTransaction().commit();
            } // if myEntityManager.getTransaction().isActive()
        } // if em != null
    }

    /**
     * @see ch.sdi.core.intf.SqlJob#rollbackTransaction()
     */
    @Override
    public void rollbackTransaction()
    {
        if ( myEntityManager != null )
        {
            if ( myEntityManager.getTransaction().isActive() )
            {
                myEntityManager.getTransaction().rollback();
            } // if myEntityManager.getTransaction().isActive()
        } // if em != null
    }

} @Entity
@Table(name="ow_base_user")
public class OxUser
{

    /*
     * Reverse engineered from a real oxwall DB (Nena1):
     *
        ow_base_user
        ------------
            INSERT INTO `ow_base_user`(`id`, `email`, `username`, `password`, `joinStamp`, `activityStamp`, `accountType`, `emailVerify`, `joinIp`)
            username: SDI-name: thing.alternateName
            password: SHA-256(salt+password)
            joinStamp: join date (unix timestamp (seconds since 1970) ) default '0'
            activityStamp: (unix timestamp (seconds since 1970) ) default '0'
            accountType: the desired account
                         Nena1: all have 290365aadde35a97f11207ca7e4279cc
                         see below ow_base_question_account_type
            emailVerify: 0 if user has not yet verified email, 1 if it is verified ( Note: (if 0) additional steps have to be done like providing a dataset in ow_base_email_verify and send the hash to the new user)
            joinIp: int(11) Nena1 example: 1535710340 (all positive)
                    PHP: ip2long(OW::getRequest()->getRemoteAddress())
                        example: (see http://php.net/manual/en/function.ip2long.php)
                            $ip   = gethostbyname('www.example.com');
                            $long = ip2long($ip);
                            if ($long == -1 || $long === FALSE) {
                                echo 'Invalid IP, please try again';
                            } else {
                                echo $ip   . "\n";           // 192.0.34.166
                                echo $long . "\n";           // -1073732954
                                printf("%u\n", ip2long($ip)); // 3221234342
                            }

                            example above reverse-engineered:
                            192.0.34.166 -> 0xC0 0x00 0x22 0xA6 -> 0xC00022A6 -> 3221234342 (unsinged) -> -1073732954 (signed)

                            real example 1535710340 (Nena1 and Pi):
                            0x5B891484 -> 0x5B 0x89 0x14 0x84 -> 91.137.20.132 (whois resolves to "Thueringer Netkom GmbH")
                            real example 3559071562 (heri):
                            0x1536B654 -> 0x15 0x36 0xB6 0x54 -> 21.54.182.84 (whois resolves to DNIC, Columbus, OH)
                                -> both resolutions do not make any sense to me!
                            real example from local network (192.168.99.110): 1234
                            0x4D2 -> 0x00 0x00 0x04 0xD2 -> 0.0.4.210
                                -> neither this makes any sense!
                -> Source V1.7.1: the joinIp is only inserted but never read. So I think it does not really matter what to insert here.

     */

    @Id
    @GeneratedValue
    private Long id;

    private String email;
    private String username;
    private String password;
    private Long joinStamp;
    private Long activityStamp;
    private String accountType;
    private boolean emailVerify;
    private Long joinIp;

    /**
     * @return email
     */
    public String getEmail()
    {
        return email;
    }

    /**
     * @param  aEmail
     *         email to set
     */
    public void setEmail( String aEmail )
    {
        email = aEmail;
    }

    /**
     * @return username
     */
    public String getUsername()
    {
        return username;
    }

    /**
     * @param  aUsername
     *         username to set
     */
    public void setUsername( String aUsername )
    {
        username = aUsername;
    }

    /**
     * @return password
     */
    public String getPassword()
    {
        return password;
    }

    /**
     * @param  aPassword
     *         password to set
     */
    public void setPassword( String aPassword )
    {
        password = aPassword;
    }

    /**
     * @return joinStamp
     */
    public Long getJoinStamp()
    {
        return joinStamp;
    }

    /**
     * @param  aJoinStamp
     *         joinStamp to set
     */
    public void setJoinStamp( Long aJoinStamp )
    {
        joinStamp = aJoinStamp;
    }

    /**
     * @return activityStamp
     */
    public Long getActivityStamp()
    {
        return activityStamp;
    }

    /**
     * @param  aActivityStamp
     *         activityStamp to set
     */
    public void setActivityStamp( Long aActivityStamp )
    {
        activityStamp = aActivityStamp;
    }

    /**
     * @return emailVerify
     */
    public boolean isEmailVerify()
    {
        return emailVerify;
    }

    /**
     * @param  aEmailVerify
     *         emailVerify to set
     */
    public void setEmailVerify( boolean aEmailVerify )
    {
        emailVerify = aEmailVerify;
    }

    /**
     * @return joinIp
     */
    public Long getJoinIp()
    {
        return joinIp;
    }

    /**
     * @param  aJoinIp
     *         joinIp to set
     */
    public void setJoinIp( Long aJoinIp )
    {
        joinIp = aJoinIp;
    }

    /**
     * @return id
     */
    public Long getId()
    {
        return id;
    }


    /**
     * @return accountType
     */
    public String getAccountType()
    {
        return accountType;
    }


    /**
     * @param  aAccountType
     *         accountType to set
     */
    public void setAccountType( String aAccountType )
    {
        accountType = aAccountType;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder( super.toString() );

        sb.append( "\n    id           : " ).append( id );
        sb.append( "\n    email        : " ).append( email );
        sb.append( "\n    username     : " ).append( username );
        sb.append( "\n    password     : " ).append( password );
        sb.append( "\n    joinStamp    : " ).append( joinStamp );
        sb.append( "\n    activityStamp: " ).append( activityStamp );
        sb.append( "\n    accountType  : " ).append( accountType );
        sb.append( "\n    emailVerify  : " ).append( emailVerify );
        sb.append( "\n    joinIp       : " ).append( joinIp );
        return sb.toString();
    }


    /**
     * @param  aId
     *         id to set
     */
    public void setId( Long aId )
    {
        id = aId;
    }



} @Entity
@Table(name="ow_base_user_disapprove")
public class OxUserUnapproved
{
    /* Reverse engineered from a real oxwall DB (Nena1):
    *
       ow_base_user_disapprove
       -----------------------
           INSERT INTO `ow_base_user_disapprove`(`id`, `userId`)
               used for
                   - an unapproved user has an entry in this table
     */

    @Id
    @GeneratedValue
    private Long id;
    private Long userId;

    /**
     * @return id
     */
    public Long getId()
    {
        return id;
    }

    /**
     * @param  aId
     *         id to set
     */
    public void setId( Long aId )
    {
        id = aId;
    }

    /**
     * @return userId
     */
    public Long getUserId()
    {
        return userId;
    }

    /**
     * @param  aUserId
     *         userId to set
     */
    public void setUserId( Long aUserId )
    {
        userId = aUserId;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder( super.toString() );

        sb.append( "\n    id     : " ).append( id );
        sb.append( "\n    userId : " ).append( userId );
        return sb.toString();
    }

}
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import ch.sdi.core.exc.SdiDuplicatePersonException;
import ch.sdi.core.exc.SdiException;
import ch.sdi.core.impl.cfg.ConfigUtils;
import ch.sdi.core.impl.data.Person;
import ch.sdi.core.impl.data.PersonKey;
import ch.sdi.core.impl.data.converter.ConverterImage;
import ch.sdi.core.impl.mail.MailJobDefault;
import ch.sdi.core.impl.mail.MailTextResolver;
import ch.sdi.core.intf.CustomTargetJobContext;
import ch.sdi.core.intf.FtpJob;
import ch.sdi.core.intf.PasswordEncryptor;
import ch.sdi.core.intf.TargetJob;
import ch.sdi.plugins.oxwall.job.OxSqlJob;
import ch.sdi.plugins.oxwall.sql.entity.OxUser;
import ch.sdi.plugins.oxwall.sql.entity.OxUserUnapproved;
/**
 * Copyright (c) 2014 by the original author or authors.
 *
 * This code 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 2.1 of the License, or (at your option) any later version.
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


package ch.sdi.plugins.oxwall;






/**
 * Implements the global business know how on the oxwall target platform.
 * <p>
 * It provides the target executor with the needed jobs.
 * <p>
 * In preparePerson which is called before a person is processed the needed data is prepared (like
 * password generation, formatting the avatar pictures) and a DB transaction is started. In finalizePerson
 * the DB transaction is committed or rollbacked (depending on the presence of an exception).
 * <p>
 * If Spring finds a implementation of CustomPreparePersonJob its execute() method is called in
 * preparePerson().
 *
 * @version 1.0 (24.11.2014)
 * @author  Heri
 */
@Component
public class OxTargetJobContext implements CustomTargetJobContext
{

    /** logger for this class */
    private Logger myLog = LogManager.getLogger( OxTargetJobContext.class );
    public static final String KEY_AVATAR_HASH = "person.avatar.hash";
    public static final String KEY_PASSWORD = "person.password";
    public static final String KEY_ENCRYPTED_PASSWORD = "person.enc_password";
    public static final String KEY_PERSON_FULLNAME = "person.fullName";
    /** the primary key of DB entity */
    public static final String KEY_PERSON_USER_ID = "person.userId";
    public static final String KEY_PERSON_PREPARED_AVATAR_FILES = "person.preparedAvatarFiles";
    public static final String KEY_NEEDS_ACTIVATION = "person.needsActivation";


    @Autowired
    private Environment myEnv;
    @Autowired
    private PasswordEncryptor myPasswordEncryptor;
    @Autowired
    private MailJobDefault myMailJob;
    @Autowired
    private FtpJob myFtpJob;