commit

commit

/*
 *  CCC - The Clipper to C++ Compiler
 *  Copyright (C) 2005 ComFirm BT.
 *
 *  This library 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 of the License, or (at your option) any later version.
 *
 *  This library 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 library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef COMMENT

inetd-szerű listener;
Windowson és UNIX-on egyformán fut;
XML paraméterfilében lehet megadni,
hogy mely portokon figyeljen,
és milyen alkalmazásokat indítson;
példa a paraméterfilére:

<xstart>
<item>
    <name>Program SSL nélkül</name>
    <if>localhost</if>
    <port>46000</port>
    <env>VAR=value</env>
    <workdir></workdir>
    <command>program.exe -jtsocket $(SOCKET)</command>
</item>

<item>
    <name>Program SSL-lel</name>
    <interface>localhost</interface>
    <port>46001</port>
    <workdir></workdir>
    <command>sslforward.exe program.exe -jtsocket $(SOCKET)</command>
</item>

</xstart>

#endif

#include "spawn.ch"

#define FAILED(txt)  (txt)+" failed (error="+alltrim(str(serror()))+")"

static proctab:={}

******************************************************************************
function main(parfile)

local wd:=dirname() //curdir nem jó
local p,s,n,i,c,arg,env,res,cmd,var

    set date format "yyyy-mm-dd"
    set printer to xstart.log additive
    set printer on

    if( parfile==NIL .or. !file(parfile) )
        ? "Usage: xstart.exe <parfile>"
        ?
        quit
    end

    p:=xmlparserNew(parfile)
    p:entityconv:=.t.
    p:processblock:={|node|node:type=="item".and.procitem(node)}
    p:parse

    for n:=1 to len(proctab)
        p:=proctab[n]
        p:socket:=hdup(socket(),.f.,.t.)

        #ifdef _UNIX_
            setsockopt(p:socket,"REUSEADDR",.t.)
            //Enélkül kellemetlen tesztelni (várni kell),
            //Windowson nem szabad beállítani, mert rossz!
        #endif

        if( 0!=bind(p:socket,p:interface,p:port) )
            ?? FAILED("bind"), p
            ?
            quit
        end

        if( 0!=listen(p:socket) )
            ?? FAILED("listen"), p
            ?
            quit
        end
    next

    while( .t. )

        s:=array( len(proctab) )
        for n:=1 to len(s)
            s[n]:=proctab[n]:socket
        next

        select(s,,,5000) //5 sec

        for n:=1 to len(s)

            i:=ascan(proctab,{|p|p:socket==s[n]})
            if( i==0 )
                loop
            end

            p:=proctab[i]
            c:=accept(s[n])

            cmd:=cmdline(p:command,c)

            if( empty(p:env) )
                env:=NIL
            else
                env:=environment()
                for i:=1 to len(p:env)
                    var:=p:env[i]
                    var:=strtran(var,"$(SOCKET)",alltrim(str(c)))
                    addenv(env,var)
                next
            end

            if( !empty(p:workdir) )
                dirchange(p:workdir)
            end

            res:=spawn(SPAWN_NOWAIT+SPAWN_PATH,cmd,env)

            if( !empty(p:workdir) )
                dirchange(wd)
            end

            ? "ACCEPT:", date(), time(), getpeername(c), p:port, p:name, res

            if(res<0)
                ?? cmd //hibás parancs
            end

            sclose(c)
        next

        #ifdef _UNIX_
            while( 0<waitpid(,,1) ) //1==WNOHANG (csak UNIX)
                //zombiek takarítása
            end
        #endif
    end

    return NIL

******************************************************************************
static function cmdline(txt,sck)

// behelyettesíti a $(SOCKET) makrót
// elvégzi az $(ENVIR) makrók helyettesítését,
// az elválasztó szóközöket egységesíti,
// visszatérés: {prog arg1 arg2...}

local n, p, cmd

    txt:=alltrim(txt)
    txt:=strtran(txt,"$(SOCKET)",alltrim(str(sck)))

    while( 0<(n:=at("$(",txt)) )  // ...$(...)...
        p:=left(txt,n-1)
        txt:=substr(txt,n+2)
        n:=at(")",txt)
        p+=getenv(left(txt,n-1))
        p+=substr(txt,n+1)
        txt:=p
    end

    txt:=strtran(txt,chr(10)," ")
    txt:=strtran(txt,chr(13)," ")
    while( "  "$txt )
        txt:=strtran(txt,"  "," ")
    end
    cmd:=split(txt," ")

    cmd[1]:=which(cmd[1]) //megkeresi a pathban

    return cmd

******************************************************************************
static function which(x) //megkeresi x-et a pathban
local p,n,f
    if( file(x) )
        //? "file exists:", x
    else
        p:=split(getenv("PATH"),pathsep())
        for n:=1 to len(p)
            if( file(f:=p[n]+dirsep()+x) )
                //? "found in path:", f
                return f
            end
        next
    end
    return x

******************************************************************************
static function addenv(env,var)  //elkerüli a duplázást!
local varnam,varpos

    varnam:=left(var,at("=",var))
    #ifdef _UNIX_
        varpos:=ascan(env,varnam)
    #else
        varpos:=ascan(env,{|x|!upper(x)!=upper(varnam)})
    #endif

    if( varpos==0 )
        aadd(env,var)
    else
        env[varpos]:=var
    end
    return env

******************************************************************************
static function procitem(node)
local n,item,attr,txt

    item:=xstartitemNew()

    for n:=1 to len( node:content )
        attr:=node:content[n]:type
        txt:=node:content[n]:gettext

        if( attr=="name" )
            item:name:=txt
        elseif( attr=="env" )
            aadd(item:env,txt)
        elseif( attr=="interface" )
            item:interface:=txt
        elseif( attr=="host" )  //interface
            item:interface:=txt
        elseif( attr=="if" )    //interface
            item:interface:=txt
        elseif( attr=="port" )
            item:port:=val(txt)
        elseif( attr=="workdir" )
            item:workdir:=txt
        elseif( attr=="wd" )    //workdir
            item:workdir:=txt
        elseif( attr=="command" )
            item:command:=txt
        else
            alert("Invalid XML tag: "+attr)
        end
    next

    if( empty(item:interface) )
        item:interface:=NIL
    end

    aadd(proctab,item)

    return .t.  //nem kell tovább építeni a fát

******************************************************************************
class xstartitem(object)
    method initialize
    attrib socket
    attrib name
    attrib env
    attrib interface
    attrib port
    attrib workdir
    attrib command

******************************************************************************
static function xstartitem.initialize(this)
    this:(object)initialize
    this:socket:=NIL
    this:name:=""
    this:env:={}
    this:interface:=NIL  //minden interfeszen figyel
    this:port:=NIL
    this:workdir:=NIL
    this:command:=NIL
    return this

******************************************************************************

/*
 *  CCC - The Clipper to C++ Compiler
 *  Copyright (C) 2005 ComFirm BT.
 *
 *  This library 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 of the License, or (at your option) any later version.
 *
 *  This library 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 library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef COMMENT

inetd-szerű listener;
Windowson és UNIX-on egyformán fut;
XML paraméterfilében lehet megadni,
hogy mely portokon figyeljen,
és milyen alkalmazásokat indítson;
példa a paraméterfilére:

<xstart>
<item>
    <name>Program SSL nélkül</name>
    <if>localhost</if>
    <port>46000</port>
    <env>VAR=value</env>
    <workdir></workdir>
    <command>program.exe -jtsocket $(SOCKET)</command>
</item>

<item>
    <name>Program SSL-lel</name>
    <interface>localhost</interface>
    <port>46001</port>
    <workdir></workdir>
    <command>sslforward.exe program.exe -jtsocket $(SOCKET)</command>
</item>

</xstart>

#endif

#include "spawn.ch"

#define FAILED(txt)  (txt)+" failed (error="+alltrim(str(serror()))+")"

static proctab:={}

******************************************************************************
function main(parfile)

local wd:=dirname() //curdir nem jó
local p,s,n,i,c,arg,env,res,cmd,var

    set date format "yyyy-mm-dd"
    set printer to xstart.log additive
    set printer on

    if( parfile==NIL .or. !file(parfile) )
        ? "Usage: xtstart <parfile>"
        ?
        quit
    end

    p:=xmlparserNew(parfile)
    p:entityconv:=.t.
    p:processblock:={|node|node:type=="item".and.procitem(node)}
    p:parse

    for n:=1 to len(proctab)
        p:=proctab[n]
        p:socket:=hdup(socket(),.f.,.t.)

        #ifdef _UNIX_
            setsockopt(p:socket,"REUSEADDR",.t.)
            //Enélkül kellemetlen tesztelni (várni kell),
            //Windowson nem szabad beállítani, mert rossz!
        #endif

        if( 0!=bind(p:socket,p:interface,p:port) )
            ?? FAILED("bind"), p
            ?
            quit
        end

        if( 0!=listen(p:socket) )
            ?? FAILED("listen"), p
            ?
            quit
        end
    next

    while( .t. )

        s:=array( len(proctab) )
        for n:=1 to len(s)
            s[n]:=proctab[n]:socket
        next

        select(s,,,5000) //5 sec

        for n:=1 to len(s)

            i:=ascan(proctab,{|p|p:socket==s[n]})
            if( i==0 )
                loop
            end

            p:=proctab[i]
            c:=accept(s[n])

            cmd:=cmdline(p:command,c)

            if( empty(p:env) )
                env:=NIL
            else
                env:=environment()
                for i:=1 to len(p:env)
                    var:=p:env[i]
                    var:=strtran(var,"$(SOCKET)",alltrim(str(c)))
                    addenv(env,var)
                next
            end

            if( !empty(p:workdir) )
                dirchange(p:workdir)
            end

            res:=spawn(SPAWN_NOWAIT+SPAWN_PATH,cmd,env)

            if( !empty(p:workdir) )
                dirchange(wd)
            end

            ? "ACCEPT:", date(), time(), getpeername(c), p:port, p:name, res

            if(res<0)
                ?? cmd //hibás parancs
            end

            sclose(c)
        next

        #ifdef _UNIX_
            while( 0<waitpid(,,1) ) //1==WNOHANG (csak UNIX)
                //zombiek takarítása
            end
        #endif
    end

    return NIL

******************************************************************************
static function cmdline(txt,sck)

// behelyettesíti a $(SOCKET) makrót
// elvégzi az $(ENVIR) makrók helyettesítését,
// az elválasztó szóközöket egységesíti,
// visszatérés: {prog arg1 arg2...}

local n, p, cmd

    txt:=alltrim(txt)
    txt:=strtran(txt,"$(SOCKET)",alltrim(str(sck)))

    while( 0<(n:=at("$(",txt)) )  // ...$(...)...
        p:=left(txt,n-1)
        txt:=substr(txt,n+2)
        n:=at(")",txt)
        p+=getenv(left(txt,n-1))
        p+=substr(txt,n+1)
        txt:=p
    end

    txt:=strtran(txt,chr(10)," ")
    txt:=strtran(txt,chr(13)," ")
    while( "  "$txt )
        txt:=strtran(txt,"  "," ")
    end
    cmd:=split(txt," ")

    cmd[1]:=which(cmd[1]) //megkeresi a pathban

    return cmd

******************************************************************************
static function which(x) //megkeresi x-et a pathban
local p,n,f
    if( file(x) )
        //? "file exists:", x
    else
        p:=split(getenv("PATH"),pathsep())
        for n:=1 to len(p)
            if( file(f:=p[n]+dirsep()+x) )
                //? "found in path:", f
                return f
            end
        next
    end
    return x

******************************************************************************
static function addenv(env,var)  //elkerüli a duplázást!
local varnam,varpos

    varnam:=left(var,at("=",var))
    #ifdef _UNIX_
        varpos:=ascan(env,varnam)
    #else
        varpos:=ascan(env,{|x|!upper(x)!=upper(varnam)})
    #endif

    if( varpos==0 )
        aadd(env,var)
    else
        env[varpos]:=var
    end
    return env

******************************************************************************
static function procitem(node)
local n,item,attr,txt

    item:=xstartitemNew()

    for n:=1 to len( node:content )
        attr:=node:content[n]:type
        txt:=node:content[n]:gettext

        if( attr=="name" )
            item:name:=txt
        elseif( attr=="env" )
            aadd(item:env,txt)
        elseif( attr=="interface" )
            item:interface:=txt
        elseif( attr=="host" )  //interface
            item:interface:=txt
        elseif( attr=="if" )    //interface
            item:interface:=txt
        elseif( attr=="port" )
            item:port:=val(txt)
        elseif( attr=="workdir" )
            item:workdir:=txt
        elseif( attr=="wd" )    //workdir
            item:workdir:=txt
        elseif( attr=="command" )
            item:command:=txt
        else
            alert("Invalid XML tag: "+attr)
        end
    next

    if( empty(item:interface) )
        item:interface:=NIL
    end

    aadd(proctab,item)

    return .t.  //nem kell tovább építeni a fát

******************************************************************************
class xstartitem(object)
    method initialize
    attrib socket
    attrib name
    attrib env
    attrib interface
    attrib port
    attrib workdir
    attrib command

******************************************************************************
static function xstartitem.initialize(this)
    this:(object)initialize
    this:socket:=NIL
    this:name:=""
    this:env:={}
    this:interface:=NIL  //minden interfeszen figyel
    this:port:=NIL
    this:workdir:=NIL
    this:command:=NIL
    return this

******************************************************************************


[Runtime] More special settings in the Set() function

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//

/// <summary>
/// Changes and/or reads a system setting.
/// </summary>
/// <param name="nDefine">Is a positive integer identifying a system setting or system variable.
/// This should match the values from the Set enumerated type.</param>
/// <param name="newValue">The optional expression can specify a new value for a system setting.
/// The data type is dependent on the system setting designated by <paramref name="nDefine" />.</param>
/// <returns>When Set() is called without the argument <paramref name="newValue" /> ,
/// the function returns the current system setting designated by <paramref name="nDefine" /> .
/// If <paramref name="newValue" /> is specified, the corresponding system setting is set to <paramref name="newValue" />
/// and the value of the old setting is returned.
/// </returns>
/// <seealso cref="T:XSharp.Set" />
/// <remarks>If you are coming from XHarbour or Xbase++ please don't use set.ch for the value of <paramref name="nDefine" />
/// because there are some differences between the values in this header file and the values used inside X#. </remarks>
FUNCTION Set(nDefine, newValue) AS USUAL CLIPPER
    LOCAL state AS XSharp.RuntimeState
    LOCAL oOld  := NULL AS OBJECT
    LOCAL nSetting AS XSharp.Set
    IF ! IsNumeric(nDefine)
        RETURN NIL
    ENDIF
    nSetting := nDefine

    IF PCount() > 1
        SWITCH nSetting
        CASE Set.DateFormat
            // Special handling, because we want to keep DateFormat, DateFormatNet and DateFormatEmpty in sync
            RETURN SetDateFormat((STRING) newValue)
        CASE Set.CharSet
            // Map SET_CHARSET to SetAnsi
            RETURN SetAnsi(newValue == 0)  // Ansi = 0, Oem == 1
        CASE Set.Ansi
            // Map SET_CHARSET to SetAnsi
            RETURN SetAnsi(newValue )
        CASE Set.Century
            // Keep date format in sync
            RETURN SetCentury(newValue )
        CASE Set.DateCountry
            RETURN SetDateCountry(newValue )
        CASE Set.DecimalSep
            RETURN SetDecimalSep(newValue)
        CASE Set.ThousandSep
            RETURN SetThousandSep(newValue)
        CASE Set.Epoch
            RETURN SetEpoch(newValue)
        CASE Set.Collation
            RETURN SetCollation(newValue)
        CASE Set.NatDLL
            RETURN SetNatDLL(newValue)
        CASE Set.International
            RETURN SetInternational(newValue)
        END SWITCH
    ENDIF

    state := XSharp.RuntimeState.GetInstance()
    IF state:Settings:ContainsKey(nSetting)
        oOld := state:Settings[nSetting]
    ENDIF
    IF PCount() > 1
        LOCAL oNew := newValue AS OBJECT
        IF oOld != NULL_OBJECT
            IF oOld IS LOGIC .AND. oNew IS STRING VAR cNew
                // XBase++ uses ON / OFF strings in stead of TRUE and FALSE
                state:Settings[nSetting] := cNew:ToUpper() == "ON"
            ELSE
               TRY
                    oNew := System.Convert.ChangeType( oNew, oOld:GetType())
                    state:Settings[nSetting] := oNew
                CATCH
                    NOP // can't convert, so ignore assignment
                END TRY
            ENDIF
        ENDIF
     ENDIF
    RETURN oOld

//
// Copyright (c) XSharp B.V.  All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//

/// <summary>
/// Changes and/or reads a system setting.
/// </summary>
/// <param name="nDefine">Is a positive integer identifying a system setting or system variable.
/// This should match the values from the Set enumerated type.</param>
/// <param name="newValue">The optional expression can specify a new value for a system setting.
/// The data type is dependent on the system setting designated by <paramref name="nDefine" />.</param>
/// <returns>When Set() is called without the argument <paramref name="newValue" /> ,
/// the function returns the current system setting designated by <paramref name="nDefine" /> .
/// If <paramref name="newValue" /> is specified, the corresponding system setting is set to <paramref name="newValue" />
/// and the value of the old setting is returned.
/// </returns>
/// <seealso cref="T:XSharp.Set" />
/// <remarks>If you are coming from XHarbour or Xbase++ please don't use set.ch for the value of <paramref name="nDefine" />
/// because there are some differences between the values in this header file and the values used inside X#. </remarks>
FUNCTION Set(nDefine, newValue) AS USUAL CLIPPER
    LOCAL state AS XSharp.RuntimeState
    LOCAL oOld  := NULL AS OBJECT
    LOCAL nSetting AS XSharp.Set
    IF ! IsNumeric(nDefine)
        RETURN NIL
    ENDIF
    nSetting := nDefine

    IF nSetting == Set.DateFormat .and. PCount() > 1
        // Special handling, because we want to keep DateFormat, DateFormatNet and DateFormatEmpty in sync
        RETURN SetDateFormat((STRING) newValue)
    ENDIF
    IF nSetting == Set.CharSet .AND. PCount() > 1
        // Map SET_CHARSET to SetAnsi
        RETURN SetAnsi(newValue == 0)  // Ansi = 0, Oem == 1
    ENDIF

    state := XSharp.RuntimeState.GetInstance()
    IF state:Settings:ContainsKey(nSetting)
        oOld := state:Settings[nSetting]
    ENDIF
    IF PCount() > 1
        LOCAL oNew := newValue AS OBJECT
        IF oOld != NULL_OBJECT
            IF oOld IS LOGIC .AND. oNew IS STRING VAR cNew
                // XBase++ uses ON / OFF strings in stead of TRUE and FALSE
                state:Settings[nSetting] := cNew:ToUpper() == "ON"
            ELSE
               TRY
                    oNew := System.Convert.ChangeType( oNew, oOld:GetType())
                    state:Settings[nSetting] := oNew
                CATCH
                    NOP // can't convert, so ignore assignment
                END TRY
            ENDIF
        ENDIF
     ENDIF
    RETURN oOld


Updated LINQ Example with JOIN clause

Updated LINQ Example with JOIN clause

USING System.Collections.Generic
USING System.Linq
USING STATIC System.Console

FUNCTION Start AS VOID
	VAR oDev   := GetDevelopers()
	VAR oC     := GetCountries()
	VAR oAll   := 	FROM D 		 IN oDev  ;
					JOIN C 		 IN oC ON D:Country EQUALS C:Name   ;
					ORDERBY D:LastName  ;
					SELECT CLASS {D:Name, D:Country, C:Region}	// Anonymous class !

	// The type of oAll is WhereSelectEnumerableIterator<<>f__AnonymousType0<Developer,Country>>
	// We prefer the VAR keyword!

	VAR oGreek := 	FROM Developer IN oDev ;
					WHERE Developer:Country == "Greece" ;
					ORDERBY Developer:LastName DESCENDING ;
					SELECT Developer

	// The type of oGreek is IOrderedEnumerable<Developer>
	// We prefer the VAR keyword!

	VAR oCount := 	FROM Developer IN oDev ;
					GROUP Developer BY Developer:Country INTO NewGroup ;
					ORDERBY NewGroup:Key SELECT NewGroup

	// The type of oCount is IOrderedEnumerable<<IGrouping<string,Developer>>
	// We prefer the VAR keyword!

	WriteLine(e"X# does LINQ!\n")
	WriteLine(e"All X# developers (country+lastname order)\n")
	FOREACH VAR oDeveloper IN oAll
		WriteLine("{0} in {1}, {2}",oDeveloper:Name,oDeveloper:Country,oDeveloper:Region)
	NEXT

	WriteLine(e"\nGreek X# Developers (descending lastname)\n")
	FOREACH oDeveloper AS Developer IN oGreek
		WriteLine(oDeveloper:Name)
	NEXT

	WriteLine(e"\nDevelopers grouped per country\n")

	FOREACH VAR country IN oCount
		WriteLine(i"{country.Key}, {country.Count()} developer(s)")
		FOREACH VAR oDeveloper IN country
			WriteLine("  " + oDeveloper:Name)
		NEXT
	NEXT
	WriteLine("Enter to continue")
	ReadLine()
	RETURN

FUNCTION GetDevelopers AS IList<Developer>
	// This function uses a collection initializer for the List of Developers
	// and Object initializers for the Developer Objects
	VAR oList := List<Developer>{} 	{ ;
										Developer{}{ FirstName  := "Chris", LastName := "Pyrgas", Country := "Greece"},;
										Developer{}{ FirstName  := "Robert", LastName := "van der Hulst", Country := "The Netherlands"},;
										Developer{}{ FirstName  := "Fabrice", LastName := "Foray", Country := "France"},;
										Developer{}{ FirstName  := "Nikos", LastName := "Kokkalis", Country := "Greece"} ;
									}
	RETURN oList

FUNCTION GetCountries AS IList<Country>
	// This function uses a collection initializer for the List of Counties
	// and Object initializers for the Country Objects
	VAR oList := List<Country>{}{ ;
									Country{} {Name := "Greece", 			Region := "South East Europe"},;
									Country{} {Name := "France", 			Region := "West Europe"},;
									Country{} {Name := "The Netherlands", 	Region := "North West Europe"} ;
								}
	RETURN oList
CLASS Developer
	PROPERTY Name 		AS STRING GET FirstName + " " + LastName
	PROPERTY FirstName 	AS STRING AUTO
	PROPERTY LastName 	AS STRING AUTO
	PROPERTY Country 	AS STRING AUTO
END CLASS

CLASS Country
	PROPERTY Name 		AS STRING AUTO
	PROPERTY Region 	AS STRING AUTO
END CLASS

USING System.Collections.Generic
USING System.Linq
USING STATIC System.Console

FUNCTION Start AS VOID
	VAR oList := GetDevelopers()
	VAR oAll   := FROM Developer IN oList ORDERBY Developer:Country, Developer:LastName SELECT Developer
	VAR oGreek := FROM Developer IN oList WHERE Developer:Country == "Greece" ORDERBY Developer:LastName DESCENDING SELECT Developer
	VAR oCount := FROM Developer IN oList GROUP Developer BY Developer:Country INTO NewGroup ORDERBY NewGroup:Key SELECT NewGroup
	WriteLine(e"X# does LINQ!\n")
	WriteLine(e"All X# developers (country+lastname order)\n")
	FOREACH oDeveloper AS Developer IN oAll
		WriteLine(e"{0} in {1}",oDeveloper:Name, oDeveloper:Country)
	NEXT

	WriteLine(e"\nGreek X# Developers (descending lastname)\n")
	FOREACH oDeveloper AS Developer IN oGreek
		WriteLine(oDeveloper:Name)
	NEXT

	WriteLine(e"\nDevelopers grouped per country\n")

	FOREACH VAR country IN oCount
		WriteLine("{0}, {1} developer(s)", country:Key, country:Count())
		FOREACH VAR oDeveloper IN country
			WriteLine("  " + oDeveloper:Name)
		NEXT
	NEXT
	WriteLine("Enter to continue")
	ReadLine()
	RETURN

FUNCTION GetDevelopers AS IList<Developer>
	VAR oList := List<Developer>{}
	oList:Add(Developer{ "Robert", "van der Hulst", "The Netherlands"})
	oList:Add(Developer{ "Chris", "Pyrgas", "Greece"})
	oList:Add(Developer{ "Fabrice", "Foray", "France"})
	oList:Add(Developer{ "Nikos", "Kokkalis", "Greece"})
	RETURN oList

CLASS Developer
	PROPERTY Name 		AS STRING GET FirstName + " " + LastName
	PROPERTY FirstName 	AS STRING AUTO
	PROPERTY LastName 	AS STRING AUTO
	PROPERTY Country 	AS STRING AUTO
	CONSTRUCTOR(cFirst AS STRING, cLast AS STRING, cCountry AS STRING)
		FirstName := cFirst
		LastName := cLast
		Country := cCountry
END CLASS