30. April 2009

Email abrufen mit PL/SQL: Package MAIL_CLIENT

English title: PL/SQL Mail Client API

Das Versenden von Emails mit PL/SQL ist ja recht einfach; dazu gibt es fertige Pakete: UTL_SMTP, UTL_MAIL und für APEX-Entwickler gibt es nochmals APEX_MAIL. Anders sieht es aus, wenn es darum geht, ein Postfach auszulesen, also Mails zu empfangen. Dazu hatte ich schon die eine oder andere Anfrage, so dass ich mich vor einiger Zeit entschlossen habe, das Thema mal anzugehen.
As most of you know sending emails from SQL or PL/SQL is not a problem. There are PL/SQL packages available out-of-the-box: UTL_SMTP, UTL_MAIL or APEX_MAIL. The latter one is intended for APEX developers. But when it is about retrieving messages from a mailbox server there is no package available. At the first glance this seems to be not possible. But wait: There is a JVM in the database and java developers know about the Java Mail API. This API is also contained in the database. Since I had some talks about such a functionality in the past I once started to build a Package MAIL_CLIENT which is just capable to receive emails.
Das Ergebnis ist ein PL/SQL-Package MAIL_CLIENT und einige Objekttypen. Die Kommunikation mit dem Mailserver via POP- oder IMAP-Protokoll wird von der Java Mail API übernommen, die dank der datenbankinternen JVM in der Datenbank vorhanden ist. Ihr könnt das Package hier herunterladen.
You can download the package here (documentation and sample scripts are contained):
Dokumentation und Beispielskripte sind im Paket enthalten. Die Nutzung ist recht einfach. Zunächst muss eine Verbindung zum Mailserver hergestellt werden.
Usage is easy: First connect to a mail server and open a folder (INBOX in most cases).
After that retrieve the mail headers ...
Nun kann man sich die vorhandenen Mails abrufen ...
Danach kann man sich eine Nachricht als MAIL_T abrufen. Der Zugriff erfolgt dabei stets über die Nachrichten-Nummer (msg_number)...
A message is being represented by MAIL_T. MAIL_CLIENT.GET_MESSAGE retrieves the message details but not the actual message content.
MAIL_T ist ein Objekttyp und steht für eine Nachricht auf dem Server. Einfache Textnachrichten kann man bspw. mit MAIL_T.GET_SIMPLE_CONTENT_CLOB abrufen. Für Multipart-Nachrichten (Mails mit Attachments) gibt es eigene Methoden. Mehr dazu in der Dokumentation.
The actual content is being retrieved with MAIL_T.GET_SIMPLE_CONTENT_CLOB. This is for simple text messages. There are special methods for Multipart messages. A messages with attachments is an example for a multipart message. See the documentation for more information.
Und schließlich wird die Verbindung zum Mailserver getrennt.
Finally disconnect from the mailserver.
Viel Spaß beim Ausprobieren ...
Have fun.

20. April 2009

Abfragen in die Vergangenheit: Flashback Query

English title: Query into the past: FLASHBACK QUERY!

Wusstet Ihr, dass man in der Oracle-Datenbank auch Änderungen zurückverfolgen kann, die bereits mit COMMIT bestätigt wurden. Flashback Query gibt es schon ziemlich lange, allerdings wird es immer noch recht selten genutzt ...
Did you know that you can review old data values even after changes were made permanent with COMMIT? And although Flashback Query is available for quite a long time now it's not used very frequently ...
Probiert mal folgendes aus:
Try the following:
SQL> delete from emp;

14 rows deleted.

SQL> commit;

commit complete.

SQL> select * from emp as of timestamp systimestamp - '5' minute;

EMPNO ENAME      JOB         MGR HIREDATE   SAL  COMM DEPTNO
----- ---------- --------- ----- -------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.80   801           20
 7499 ALLEN      SALESMAN   7698 20.02.81  1601   300     30
 7521 WARD       SALESMAN   7698 22.02.81  1251   500     30
 7566 JONES      MANAGER    7839 02.04.81  2975           20
 7654 MARTIN     SALESMAN   7698 28.09.81  1251  1400     30
 7698 BLAKE      MANAGER    7839 01.05.81  2850           30
 7782 CLARK      MANAGER    7839 09.06.81     0           10
 7788 SCOTT      ANALYST    7566 09.12.82  3000           20
 7839 KING       PRESIDENT       17.11.81 76000           10
 7844 TURNER     SALESMAN   7698 08.09.81  1501     0     30
 7876 ADAMS      CLERK      7788 12.01.83  1101           20
 7900 JAMES      CLERK      7698 03.12.81   951           30
 7902 FORD       ANALYST    7566 03.12.81  3000           20
 7934 MILLER     CLERK      7782 23.01.82  1301           10

14 rows selected.

SQL> insert into emp (select * from emp as of timestamp systimestamp - '5' minute);

14 rows created.
Das Beispiel zeigt, wie man eine Änderung rückgängig machen kann, obwohl sie schon mit COMMIT "festgemacht" wurde. Es wird nur eine zusätzliche SQL-Klausel benötigt ... mehr nicht. Flashback Query wurde bereits mit Oracle9i eingeführt. Der Hintergrund ist der, dass die nötigen Undo-Informationen ohnehin anfallen. Solange das COMMIT noch nicht erfolgt ist, sehen andere Datenbanksitzungen stets den alten Stand der Daten vor der Änderung (before images) - In der Oracle-Datenbank blockiert eine schreibende Sitzung niemals eine lesende und umgekehrt.
This example shows how a committed DELETE operation can be undone. Just an additional SQL clause is necessary. The Flashback Query feature was introduced in Oracle9i. It's based on the UNDO management of the Oracle database. When one session manipulated data other sessions see the old values (before images) as long the first session has not issued the COMMIT command. In Oracle a reading session never blocks a writing one and vice versa.
Nach dem COMMIT werden die alten Daten nun mitnichten zerstört - sie werden lediglich zum Überschreiben freigegeben. Wann dieses Überschreiben nun tatsächlich stattfindet, hängt von der Transaktionslast auf dem Datenbanksystem ab. Die Before Images werden in den Undo-Segmenten und diese wiederum in den Undo Tablespaces gespeichert. Man muss sich das Schreiben in einem Undo-Segment "rollierend" vorstellen; wenn die Datenbank am Ende angelangt ist, fängt sie mit der Suche nach dem nächsten freien Platz wieder von vorne an. Und erst wenn die Before Images tatsächlich überschrieben wurden, sind sie weg - vorher können Sie noch genutzt werden ... beispielsweise zum Betrachten älterer Datenstände, wie oben.
After the COMMIT was issued those Before Images don't get explicitly deleted. They're just marked for overwriting. They get actually overwritten when the undo space they occupy is needed - and this depends on the transaction load of the system. All undo data is stored in the undo segments and those are stored in the undo tablespace. Imagine the writing in those undo segments as a "rolling" process. When the end of the segment is reached it starts looking for free space at the beginning. So the before images available as long they don't get overwritten. And as long they're available they can be used: to look into "old" data versions as shown above ...
Je größer die Undo-Segmente nun gestaltet werden (je größer das Undo-Tablespace werden darf), desto weiter kann man in der Vergangenheit zurückgehen. Auf einem Produktionssystem mit hoher Last sind wahrscheinlich 10 bis 15 Minuten das äußerste der Gefühle, auf einem weniger stark belasteten System kann es durchaus mehr werden ...
The larger the undo segments may grow the longer a query "may reach into the past". On a production system with high load this might be only a few minutes, on a system with low load the timeframe might be greater.
Wozu ist das nun gut ...?
So what can this be used for ...?
Man kann dem Endanwender in der Applikation die Möglichkeit geben, Daten zu einem Zeitpunkt der Vergangenheit zu selektieren (bspw. 5 Minuten vorher). Damit bekommt dieser die Möglichkeit, einen eventuell gemachten Fehler (Daten versehentlich gelöscht) selbst zu korrigueren. Wo man ansonsten die Daten aus einem Backup-System zurückspielen müsste, reicht hier eine einfache SQL-Klausel. Aber damit die Endnutzer davon profitieren können, müssen die Anwendungsentwickler Flashback Query in die Applikation einbauen.
With the Flashback Query capability an application developer might allow the end user to query data as of a point of time in the past. The practical benefit is that end users then could correct errors themselfes. When, for instance, an end user accidentially deleted a record they can use the flashback feature to query the data as of before the deletion and optionally directly recover it. Without such a capability this would require to use a backup system, lookup the data there and recover it into the production system. With flashback query this is just the application of a SQL clause - but developers have to leverage it into the applications before the end users can benefit from it.
Datenbankparameter
Database parameters
In diesem Zusammenhang interessant ist der Parameter UNDO_RETENTION, der vom DBA gesetzt werden kann (Doku). Damit lässt sich festlegen, wie weit man "in der ergangenheit zurückgehen" möchte. Standardmäßig steht er auf 900 Sekunden, also 15 Minuten. Die Datenbank wird die UNDO-Segmente und -Tablespaces nun so verwalten, dass diese Vorgabe nach Möglichkeit eingehalten wird. Ist die AUTOEXTEND-Klausel des Undo-Tablespace aktiv, so wird er entsprechend wachsen.
Most interesting is the database parameter UNDO_RETENTION. This parameter (Documentation) might be set by the DBA and controls the amount of time a query might "reach into the past". The default is 900 (seconds), which means 15 Minutes. The database adjusts the undo segments and tablespaces in order to fulfil this reqiurement. If the AUTOEXTEND-clause is activated for the undo tablespace it will grow repectively.
Wenn das UNDO-Tablespace nicht (mehr) wachsen kann, werden die Daten bei Bedarf jedoch überschrieben; UNDO_RETENTION ist also eher als "Hinweis an die Datenbank" und nicht als "Garantie" zu verstehen (das könnte man mit der RETENTION GUARANTEE-Klausel beim CREATE UNDO TABLESPACE-Kommando auch ändern).
If the Undo Tablespace cannot grow (either because there is no space left on the device or the AUTOEXTEND clause is deactivated) then the undo information will be overwritten even if it's not older than the specified undo retention period. So the UNDO_RETENTION parameter is considered as a hint, not as a "hard" parameter. This might be changed with the RENTENTION GUARANTEE clause in the CREATE UNDO TABLESPACE command.
Mehr zu Flashback Query in den nächsten Blog Postings. Die Flashback-Technologie ist in den Versionen seit Oracle9i stetig ausgebaut worden und es geht noch wesentlich mehr als das oben gezeigte ...
More about the Flashback Query technology will follow in the next blog posting. The feature was further developed since Oracle9i and there is a lot more functionality available than shown in the beginning.
Übrigens: In Application Express steht Flashback Query bei den Interaktiven Berichten bereit. Einfach im Menü Flashback auswählen und die Zeit eingeben, die man "zurück" möchte. Genau so sollte Flashback in allen Anwendungen bereitgestellt werden. Der Endanwender legt einfach nur fest, dass er "in die Vergangenheit" abfragen möchte ...
BTW: Application Express leverages Flashback Query in its interactive reports. Just navigate to the menu (for end users), choose Flashback and enter the amount of minutes you want "to go into the past". That's the way I'd like to see Flashback in business applications: End users just have to choose to use it ...

2. April 2009

In Windows, SQL und PLSQL: "oerr" für alle!

English title: Windows, SQL and PL/SQL: "oerr" for everyone!

Wenn ich bei einem ORA-Fehler nur die Fehlernummer habe, benutze ich (auf Unix/Linux) gerne das oerr-Utility, um mir die Meldung anzeigen zu lassen (die meisten von euch kennen das Tool sicherlich) ...
When I encounter an ORA error having only the error number I like using the oerr utility to get the message and some additional information (the most of you might know this tool) ....
$ oerr ora 3113
03113, 00000, "end-of-file on communication channel"
// *Cause: The connection between Client and Server process was broken.
// *Action: There was a communication error that requires further investigation.
//          First, check for network problems and review the SQL*Net setup.
//          Also, look in the alert.log file for any errors. Finally, test to
//          see whether the server process is dead and whether a trace file
//          was generated at failure time.
Schade nur, dass man am UNIX angemeldet sein muss und das das Tool auf Windows gar nicht zur Verfügung steht ... Schöner wäre es ja, wenn es auf allen Plattformen eine SQL-Funktion oerr gäbe, mit dem man sich die Informationen aus der SQL-Ebene heraus anzeigen lassen könnte.
But the utility is only available on UNIX or Linux systems, and you have to be logged into the shell in order to use it. But would be much nicer when to tool would reside in the database and could be accessed with a SQL query.
Dazu zuerst ein paar Gedanken zur Arbeitsweise des oerr-Tools. Alle Fehlermeldungen sind in Dateien im Verzeichnisbaum des ORACLE_HOME abgelegt. Es gibt binäre Dateien mit der Endung msb und Klartextdateien mit der Endung msg - letztere nur auf Unix/Linux. Die Dateien befinden sich immer in einem Verzeichnis mesg und haben das Namensschema "{facility}us.msg". Die ORA-Fehlermeldungen sind also im Verzeichnis {...}/mesg in der Datei oraus.msg. Eine Liste aller "facilities" ist in der Datei $ORACLE_HOME/lib/facilities.lis zu finden. Das oerr-Tool liest diese Dateien; öffnet die richtige msg-Datei und filtert mit einem awk-Befehl die gewünschte Fehlermeldung aus.
First some thoughts about how the oerr utility does work: All error messages are in files distributed over the $ORACLE_HOME directory tree. There are binary files with the msb extension and clear text files with the msg extension. The latter ones are only available on Unix/Linux systems. All files are in folders named mesg in various subfolders under $ORACLE_HOME. The filename is always "{facility}us.msg" - so the ORA error messages reside in the file oraus.msg. A listing of all mesg directories containing message files is in the file $ORACLE_HOME/lib/facilities.lis. The oerr utility parses this file, opens the correct message file and filters the desired information with a simple awk command.
Ein erster Gedanke von mir war, die Arbeitsweise von oerr mit Java in der Datenbank nachzubilden, also anhand der facility und der Fehlernummer zuerst in der facilities.lis nachzusehen, dann die richtige Datei zu öffnen und die gewünschte Fehlermeldung darin zu suchen. Damit hätte man die Funktionalität in die Datenbank genommen - sie würde aber nach wie vor nicht auf Windows bereitstehen ... nicht so gut ...
My first thought was to mimic the semantics of the oerr tool using java in the database: open the facilties.lis file, determine the correct subdirectory, open the correct msg file and lookup the information for the desired error message. This would make the functionality available to the SQL layer. But it would still be restricted to UNIX/Linux systems.
Also habe ich mich anders entschieden: Man könnte die Dateien ja auch komplett durchparsen und die Inhalte in eine Datenbanktabelle speichern - die könnte man anschließend mit Export/Import in eine beliebige Datenbank übertragen und hätte die oerr-Funktion damit in jeder beliebigen Datenbank.
So I came to another idea: The files are being be completely and the error message information is being stored in a database table. This table can then be moved to another (windows) database using export/import. This would enable the oerr functionality in every database, independent of the platform.
Also zunächst als SYS das folgende SQL-Skript laufen lassen (ein andere User geht natürlich auch). Es erzeugt die Tabelle ERROR_MESSAGES und eine Funktion OERR zum Abrufen der Informationen. Zum Abschluß nur noch ein PUBLIC SYNONYM einrichten und EXECUTE-Privilegien an PUBLIC geben ...
First install as SYS the following SQL script (using another database user is also possible). It creates the table ERROR_MESSAGES and a PL/SQL function OERR to retrieve the information for a particular error message. Finally create a public synonym and grant execute privileges to public.
drop table error_messages purge
/

drop public synonym oerr
/

create table error_messages (
  facility    varchar2(20),
  errornum    number(5),
  message     varchar2(1000),
  description varchar2(4000),
  constraint pk_error_messages primary key (facility, errornum)
)
/

create or replace function oerr (
  p_facility error_messages.facility%TYPE,
  p_errornum error_messages.errornum%TYPE
) return varchar2 is
  v_message varchar2(4000);
begin
  begin
    select 
      upper(facility) || '-' || lpad(to_char(errornum),5,'0') || ':' || message || chr(10) || description
      into v_message
    from error_messages 
    where facility = p_facility and errornum = p_errornum;
  exception 
    when NO_DATA_FOUND then
      v_message := 'Error Message "' || upper(p_facility) || '-' || lpad(to_char(p_errornum), 5, '0') || '" not found.';
  end;
  return v_message;
end;
/
sho err

grant execute on oerr to public
/

create public synonym oerr for oerr
/
Und diese Tabelle muss man nun mit den Fehlermeldungen füllen. Das besorgt das folgende Java-Programm. Erst mal kopieren (sind ein paar Codezeilen) und in eine Datei OerrInstall.java speichern.
Now the table has to be populated. This is done by the following java program. Copy (it has some lines of code) and store it into a file named OerrInstall.java.
import java.util.StringTokenizer;

import java.io.File;
import java.io.FileReader;
import java.io.FileInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

import java.util.Properties;

import java.sql.*;

public class OerrInstall {
  private String sOracleHome = null;
  private String sUsername = null;
  private String sPassword = null;
  private String sConnectionString = null;
  private boolean bSysdba = false;

  private Connection con = null;
  private PreparedStatement pstmt = null;
    

  public static void main (String args[]) throws Exception {
    String sUsername = null;
    String sPassword = null;
    String sConnectionString = null;
    String sOracleHome = null;
    boolean bSysdba = false;


    for (int i=0;i<args.length;i++) {
      if (args[i].startsWith("user")) {
        sUsername = args[i].substring(args[i].indexOf("=") + 1);
      }
      if (args[i].startsWith("password")) {
        sPassword = args[i].substring(args[i].indexOf("=") + 1);
      }
      if (args[i].startsWith("connstr")) {
        sConnectionString = args[i].substring(args[i].indexOf("=") + 1);
      }
      if (args[i].equals("sysdba")) {
        bSysdba = true;
      }
      if (args[i].startsWith("oraclehome")) {
        sOracleHome = args[i].substring(args[i].indexOf("=") + 1);
      }
    }
    if (sUsername == null || sPassword == null || sConnectionString == null || sOracleHome == null) {
      System.out.println("Usage example:");
      System.out.println("$ java OerrInstall user=sys password=oracle sysdba oraclehome=[orahome] connstr=localhost:1521:orcl");
      System.exit(0);
    }
    OerrInstall oi = new OerrInstall(sUsername, sPassword, sConnectionString, bSysdba, sOracleHome);
  }

  public OerrInstall(
    String psUsername, 
    String psPassword, 
    String psConnectionString, 
    boolean pbSysdba, 
    String psOracleHome
  ) throws Exception {
    sUsername = psUsername;
    sPassword = psPassword;
    sConnectionString = psConnectionString;
    bSysdba = pbSysdba;
    sOracleHome = psOracleHome;

    DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
    Properties p = new Properties();
    p.put("user", sUsername);
    p.put("password", sPassword);
    if (bSysdba) {
      p.put("internal_logon", "sysdba");
    }
    con = DriverManager.getConnection("jdbc:oracle:thin:@" + sConnectionString, p);
    con.setAutoCommit(false);
    pstmt = con.prepareStatement(
      "insert into error_messages (" +
      "  facility, errornum, message, description" + 
      ") values (" + 
      "  ?,?,?,?" + 
      ")" 
    );
    
    doit(con);

    pstmt.close();
    con.commit();
    con.close();
  }
 
  private void doit(Connection con) throws Exception {

    boolean bMessageInProcess = false;


    String       sFacility = null;
    int          iMessageNum = 0;
    int          iNewMessageNum = 0;
    String       sMessage = null;
    StringBuffer sDescription = new StringBuffer();

    BufferedReader oFacReader = null;
    File           oMsgFile = null;
    BufferedReader oMsgReader = null;
    String sSubDir = null;
    String sLine = null;
    StringTokenizer st = null;
    int    iMessageCount = 0;
    

    
    /* outer loop: Read the facility list ... */
    oFacReader  = new BufferedReader(
      new FileReader(sOracleHome + File.separator + "lib" + File.separator + "facility.lis")
    );
    while ( (sLine = oFacReader.readLine()) != null) {
      if (! (sLine.startsWith("#") || sLine.length() == 0)) {
        st = new StringTokenizer(sLine, ":");
        sFacility = st.nextToken();
        sSubDir   = st.nextToken();

        oMsgFile = new File(
           sOracleHome + File.separator + 
           sSubDir     + File.separator + 
           "mesg"      + File.separator + 
           sFacility+"us.msg"
        );
        if (oMsgFile.exists()) {
         System.out.print("Entering message file for facility \"" + sFacility.toUpperCase() + "\" ... ");
         oMsgReader  = new BufferedReader(
          new InputStreamReader(
           new FileInputStream(  
            sOracleHome + File.separator + 
            sSubDir     + File.separator + 
            "mesg"      + File.separator + 
            sFacility+"us.msg"
           ),
           "us-ascii"
          )
         );

         /* inner loop: parse the message file ... */
         bMessageInProcess = false;
         iMessageCount = 0;
         while ( (sLine = oMsgReader.readLine()) != null) {
         try {
           iNewMessageNum = Integer.parseInt(sLine.substring(0,sLine.indexOf(","))); 
           if (bMessageInProcess) {
             storeMessage(sFacility, iMessageNum, sMessage, sDescription.toString());
           }  
           iMessageNum = iNewMessageNum;
           bMessageInProcess = true;
           sDescription.setLength(0);
           iMessageCount++;

           st = new StringTokenizer(sLine, ",");
           st.nextToken();
           st.nextToken();
           sMessage = st.nextToken();
         } catch (Exception e) {
           if (sLine.startsWith("//")) {
             if (bMessageInProcess) {
               sDescription.append(sLine).append("\n");
             } 
           } else {
             if (bMessageInProcess) {
               bMessageInProcess = false;
               storeMessage(sFacility, iMessageNum, sMessage, sDescription.toString());
             } 
           }
         }
        }
        oMsgReader.close();
        if (bMessageInProcess) {
          storeMessage(sFacility, iMessageNum, sMessage, sDescription.toString());
          iMessageCount++;
        }  
        System.out.println( iMessageCount + " messages.");
       }
      }
    }
    oFacReader.close();
    System.out.println("\nFinished.");
  }

  private void storeMessage(
    String sFacility, 
    int    iMessageNum, 
    String sMessage,
    String sDescription
  ) throws Exception {
    pstmt.setString(1, sFacility);
    pstmt.setInt(2, iMessageNum);
    pstmt.setString(3, sMessage);
    pstmt.setString(4, sDescription);
    pstmt.execute();
 }
}
Dann kompilieren (die Klassen des Oracle-JDBC-Treibers müssen im CLASSPATH sein) ...
Now compile it (the Oracle jdbc classes must be present in the java CLASSPATH)
$ javac OerrInstall.java
Und aufrufen ... Der Aufruf muss auf einem Unix/Linux-System erfolgen, denn nur dort sind die Klartextdateien, die geparst werden, vorhanden. Die Zieldatenbank, die mit dem Parameter connstr festgelegt wird und in die die Daten gespeichert werden, kann aber durchaus eine entfernte Windows-Datenbank sein ...
Start the program. Since the clear text message files which are being parsed are only present on UNIX/Linux system, The java program must be executed there. But the target database which is determined by the connstr parameter might be any other database (also on Windows platforms).
$ java OerrInstall user=SYS \
                   password=[syspw] \ 
                   sysdba 
                   connstr=[host]:[port]:[orasid] \
                   oraclehome=[absoluter pfad zum ORACLE_HOME auf dem Unix-System]
Auch hier muss nicht unbedingt SYS verwendet werden, ein anderer User geht auch. Es muss aber der sein, unter dem vorher das SQL-Skript eingespielt wurde. Lasst dann einfach den Parameter sysdba weg. Ihr seht dann etwa folgende Ausgabe ..
You also don't have to use SYS here. But you might choose the same account into which you installed the SQL script before. When you don't use SYS omit the parameter sysdba. You'll see the following output ...
Entering message file for facility "AUD" ... 19 messages.
Entering message file for facility "CLSR" ... 58 messages.
:
:
Entering message file for facility "UDI" ... 157 messages.
Entering message file for facility "VID" ... 13 messages.

Finished.
Und das war's ... Nun einfach auf die Datenbank, die mit connstr angegeben wurde, verbinden und mit SQL*Plus ausprobieren ...
That's it. Now connect to the database which you have specified in the connstr parameter and try the new function ...
SQL> select oerr('ora',31001) from duaL;

OERR('ORA',31001)
--------------------------------------------------------------------------------
ORA-31001: "Invalid resource handle or path name \"%s\""
// *Cause:   An invalid resource handle or path name was passed to
//           the XDB hierarchical resolver.
// *Action:  Pass a valid resouce handle or path name to the hierarchical
//           resolver.
Viel Spaß damit ...
Have fun ...

Beliebte Postings