Monday, June 10, 2013

Translate From/To MySQL and Oracle SQL Dialects in Java

I needed this feature when using MySQL Workbench which is a free tool to design, develop and administer data bases. The tool permits to graphically design an Enhanced Entity-Relation (EER) model then to generate the corresponding MySQL script or even to directly deploy it to a running MySQL database schema. Despite the fact it is an Oracle product, MySQL Workbench does not (yet ?) provide the feature of generating Oracle SQL dialect scripts given an EER model description.

In the following we provide a generic translator that takes a set of translation rules and performs the translation on a given input String. Two more or less complete sets permitting to translate MySQL from/to Oracle SQL dialects are also provided.

The Eclipse Project tree looks like:


1 - Package trans.util:

This package contains useful classes.

1.1 - Strings.java:


package trans.util;

import java.util.ArrayList;

public abstract class Strings
{
 public static ArrayList<String> string2Lines(String in)
 {
  ArrayList<String> result = new ArrayList<String>();
  
  for (String s : in.trim().split("\n"))
  {
   if ((s != null) && !s.isEmpty())
   {
    result.add(s);
   }
  }
  
  return(result);
 }
 
 public static String lines2String(ArrayList<String> lines)
 {
  String result = "";
  
  for (String s : lines)
  {
   result += s + "\n";
  }
  
  return(result);
 }
}

This abstract class provides methods splitting one String content to several lines and vice versa.

1.2 - Files.java:


package trans.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public abstract class Files
{
 public static String LINE_SEPARATOR = System.getProperty("line.separator");
 public static String FILE_SEPARATOR = System.getProperty("file.separator");
 
 public static void string2File(String s, String path) throws IOException 
 {
  FileWriter fw = new FileWriter(path);
  BufferedWriter bw = new BufferedWriter(fw);
  bw.write(s);
  bw.close();
  fw.close();
 }
 
 public static String file2String(String filename) throws IOException 
 {
  FileReader fr = new FileReader(filename);
  BufferedReader br = new BufferedReader(fr);
  StringBuilder result = new StringBuilder();
  String line;
  
  while ((line = br.readLine()) != null) 
  {
   result.append(line);
   result.append(LINE_SEPARATOR);
  }
  
  br.close();
  fr.close();
  
  return result.toString();
 }
}

This class provides methods returning the content of a file as a String and storing some String content in a file given its path. This class is not really needed by the translation logic.

2 - Package trans.rules:

This package contains an abstract class TranslationRules.java that defines translation rules.

package trans.rules;

import java.util.HashMap;
import java.util.Map;

public abstract class TranslationRules
{
 public static String COMMENT_DELETED_LINES_PREFIX = "-- NOT TRANSLATED : ";
 
 private String[] deleteLinesStartingWith;
 public String[] getDeleteLinesStartingWith() {
  return deleteLinesStartingWith;
 }
 public void setDeleteLinesStartingWith(String[] deleteLinesStartingWith) {
  this.deleteLinesStartingWith = deleteLinesStartingWith;
 }
 
 private boolean commentDeletedLines;
 public boolean isCommentDeletedLines() {
  return commentDeletedLines;
 }
 public void setCommentDeletedLines(boolean commentDeletedLines) {
  this.commentDeletedLines = commentDeletedLines;
 }
 
 private String[] delete;
 public String[] getDelete() {
  return delete;
 }
 public void setDelete(String[] delete) {
  this.delete = delete;
 }
 
 private Map <String, String> replace = new HashMap <String,String >();
 public Map <String, String> getReplace() {
  return replace;
 }
 public void setReplace(Map <String, String > replace) {
  this.replace = replace;
 }
 
 private Map <String, String> replaceMany = new HashMap <String,String >();
 public Map <String, String> getReplaceMany() {
  return replaceMany;
 }
 public void setReplaceMany(Map <String, String > replaceMany) {
  this.replaceMany = replaceMany;
 }
 
 public TranslationRules(String[] deleteLinesStartingWith, String[] delete, Map <String, String> replace, Map <String, String> replaceMany)
 {
  this.deleteLinesStartingWith = deleteLinesStartingWith;
  this.commentDeletedLines = false;
  this.delete = delete;
  this.replace = replace;
  this.replaceMany = replaceMany;
 }
}

The class defines the following properties:

- String[] deleteLinesStartingWith: This array defines the regular expression patterns which when matched at the beginning of a line of the input to be translated will result in the line being deleted or commented (with the prefix in String COMMENT_DELETED_LINES_PREFIX) if boolean commentDeletedLines is set to true.

- String[] delete: This array defines the regular expression patterns which when matched inside the input to be translated will be deleted (replaced by the empty String).

- Map <String, String> replace: This map defines the regular expression patterns (in the map keys) which when matched inside the input to be translated will be transformed (to the corresponding map values).

Map <String, String> replaceMany: Similar to the previous but may need several replacement iterations.

The class TranslationRules.java is implemented by two concrete classes.

- Oracle2MySQL.java:

package trans.rules;

import java.util.HashMap;
import java.util.Map;

public class Oracle2MySQL extends TranslationRules
{
 public static String[] ORACLE2MYSQL_DELETE_LINES_STARTING_WITH = {"^(CREATE|create|Create)(\\s+)(USER|user|User)",
                  "^(GRANT|grant|Grant)"};
 public static String[] ORACLE2MYSQL_DELETE = {};
 public static Map <String, String > ORACLE2MYSQL_REPLACE = new HashMap <String, String>();
 static
 {
  ORACLE2MYSQL_REPLACE.put("NUMERIC", "DECIMAL");
  ORACLE2MYSQL_REPLACE.put("NUMBER", "DECIMAL");
  ORACLE2MYSQL_REPLACE.put("VARCHAR2", "VARCHAR");
 }
 public static Map <String, String> ORACLE2MYSQL_REPLACE_MANY = new HashMap <String, String>();
 static
 {
  ORACLE2MYSQL_REPLACE_MANY.put("(CONSTRAINT|constraint|Constraint)(\\s+)(\\w+)(\\s+)(UNIQUE|unique|Unique)(\\s*)\\((\\s*)(\\w+)(.*)\\)","UNIQUE INDEX $3 ($8)");
 }
 
 public Oracle2MySQL()
 {
  super(ORACLE2MYSQL_DELETE_LINES_STARTING_WITH, ORACLE2MYSQL_DELETE, ORACLE2MYSQL_REPLACE, ORACLE2MYSQL_REPLACE_MANY);
 }
 
 public Oracle2MySQL(boolean commentDeletedLines)
 {
  super(ORACLE2MYSQL_DELETE_LINES_STARTING_WITH, ORACLE2MYSQL_DELETE, ORACLE2MYSQL_REPLACE, ORACLE2MYSQL_REPLACE_MANY);
  setCommentDeletedLines(commentDeletedLines);
 }
}


MySQL2Oracle.java:

package trans.rules;

import java.util.HashMap;
import java.util.Map;

public class MySQL2Oracle extends TranslationRules
{
 public static String[] MYSQL2ORACLE_DELETE_LINES_STARTING_WITH = {"^(SET|set|Set)", 
                  "^(USE|use|Use)",
                  "^(CREATE|create|Create)(\\s+)(SCHEMA|schema|Schema)",
                  "^(CREATE|create|Create)(\\s+)(USER|user|User)",
                  "^(GRANT|grant|Grant)"};
 public static String[] MYSQL2ORACLE_DELETE = {"`mysql`\\.",
             "(ON|on|On)(\\s+)(DELETE|delete|Delete)(\\s+)(((NO|no|No)(\\s+)(ACTION|action|Action))|CASCADE|cascade|Cascade|RESTRICT|restrict|Restrict|((SET|set|Set)(\\s+)(NULL|null|Null)))",
             "(ON|on|On)(\\s+)(UPDATE|update|Update)(\\s+)(((NO|no|No)(\\s+)(ACTION|action|Action))|CASCADE|cascade|Cascade|RESTRICT|restrict|Restrict|((SET|set|Set)(\\s+)(NULL|null|Null)))",
             "(IF|if|If)(\\s+)(EXISTS|exists|Exists)",
             "(IF|if|If)(\\s+)(NOT|not|Not)(\\s+)(EXISTS|exists|Exists)",
             "(ENGINE|engine|Engine)(\\s+)=(\\s+)(INNODB|innodb|InnoDB)",
             "(ENGINE|engine|Engine)(\\s+)=(\\s+)(MYISAM|myisam|MyIsam)",
             "`"};
 
 public static Map <String, String> MYSQL2ORACLE_REPLACE = new HashMap <String, String>();
 static
 {
  MYSQL2ORACLE_REPLACE.put("DECIMAL", "NUMERIC");
  MYSQL2ORACLE_REPLACE.put("VARCHAR", "VARCHAR2");
 }
 
 public static Map <String, String> MYSQL2ORACLE_REPLACE_MANY = new HashMap <String, String>();
 static
 {
  MYSQL2ORACLE_REPLACE_MANY.put("(UNIQUE|unique|Unique)(\\s+)(INDEX|index|Index)(\\s+)(\\w+)(\\s*)\\((\\s*)(\\w+)(\\s+ASC|asc|DESC|desc)?\\)"
         , "CONSTRAINT $5 UNIQUE ($8)");
  MYSQL2ORACLE_REPLACE_MANY.put("(CREATE|create|Create)(\\s+)(TABLE|table|Table)(\\s+)(IF NOT EXISTS|if not exists|If Not Exists)?(\\s+)(\\w+)(\\s+)\\(([^;]+)(INDEX|index|Index)(\\s+)(\\w+)(\\s*)\\((\\s*)(\\w+)(\\s+ASC|asc|DESC|desc)?(\\s*)\\)(\\s*),([^;]+)\\)(\\s+);"
         ,"$1$2$3$4$5$6$7$8($9$19)$20;\nCREATE INDEX $12 ON $7($15);");
 }
 
 public MySQL2Oracle()
 {
  super(MYSQL2ORACLE_DELETE_LINES_STARTING_WITH, MYSQL2ORACLE_DELETE, MYSQL2ORACLE_REPLACE, MYSQL2ORACLE_REPLACE_MANY);
 }
 
 public MySQL2Oracle(String schema)
 {
  super(MYSQL2ORACLE_DELETE_LINES_STARTING_WITH, MYSQL2ORACLE_DELETE, MYSQL2ORACLE_REPLACE, MYSQL2ORACLE_REPLACE_MANY);
  getDelete()[0] = "`" + schema + "`\\.";
 }
 
 public MySQL2Oracle(boolean commentDeletedLines)
 {
  super(MYSQL2ORACLE_DELETE_LINES_STARTING_WITH, MYSQL2ORACLE_DELETE, MYSQL2ORACLE_REPLACE, MYSQL2ORACLE_REPLACE_MANY);
  setCommentDeletedLines(commentDeletedLines);
 }
 
 public MySQL2Oracle(String schema, boolean commentDeletedLines)
 {
  super(MYSQL2ORACLE_DELETE_LINES_STARTING_WITH, MYSQL2ORACLE_DELETE, MYSQL2ORACLE_REPLACE, MYSQL2ORACLE_REPLACE_MANY);
  getDelete()[0] = "`" + schema + "`\\.";
  setCommentDeletedLines(commentDeletedLines);
 }
}

Comments: Static instances are provided to instantiate inherited properties for the both classes above.
Off course these Arrays and Maps can be enriched to match you custom expectations, here are some advice if you want to extend them:

MYSQL2ORACLE_DELETE_LINES_STARTING_WITH: Here you have the choice to either provide regular expressions (prefixed by ^ which enforces matching the pattern at the beginning of the line) or a simple String. You do not need to matter about trailing blank space characters at the beginning of the line since each line of the input will be trimmed before treatment.

MYSQL2ORACLE_DELETE: Here regular expressions are expected. Off course constant String patterns can be provided since they are also regular expression and if you want this just pay attention to escape characters having special meanings for regular expressions: example don't use "." if you want to match a simple dot but rather "\\." since the dot means matching any character in a regular expression.

MYSQL2ORACLE_REPLACE: Here also regular expressions (including constant Strings) are expected. For example in line 26 we add the entry that will transform each VARCHAR (MySQL syntax) occurrence by VARCHAR2 (Oracle syntax).

- MYSQL2ORACLE_REPLACE_MANY: Here also regular expressions (including constant Strings) are expected. Example in lines 34-35 we provide the way to extract an inner foreign key index declaration from a CREATE TABLE query and append it to its end. Since only one foreign key index declaration will be matched by the regular expression, there should be as many replacements as foreign key index declarations.

One important parameter for the MySQL2Oracle.java is the schema. This helps in removing all the schema references in the script. For both classes the property commentDeletedLines can be set upon construction.

3 - Package trans.engine:

This package contains the generic class that will perform the translation given a TranslationRules instance.

package trans.engine;

import java.io.IOException;
import java.util.Map;
import trans.rules.MySQL2Oracle;
import trans.rules.TranslationRules;
import trans.util.Files;
import trans.util.Strings;

public class TranslationAgent
{
 private TranslationRules translationRules;
 public TranslationRules getTranslationRules() {
  return translationRules;
 }
 public void setTranslationRules(TranslationRules translationRules) {
  this.translationRules = translationRules;
 }

 public TranslationAgent(TranslationRules translationRules)
 {
  this.translationRules = translationRules;
 }
 
 public String deleteLinesStartingWithBadItems(String in)
 {
  String result = "";
  
  for (String s : Strings.string2Lines(in))
  {
   boolean goodLine = true;
   
   for (String bad : getTranslationRules().getDeleteLinesStartingWith())
   {
    if ((!bad.startsWith("^") && s.trim().startsWith(bad) || (bad.startsWith("^") && s.trim().matches(bad+"(.+)"))))
    {
     goodLine = false;
     break;
    }
   }
   
   if (goodLine)
   {
    result += s + "\n";
   }
   else
   {
    if (getTranslationRules().isCommentDeletedLines())
    {
     result += TranslationRules.COMMENT_DELETED_LINES_PREFIX + s + "\n";
    }
   }
  }
  
  return(result);
 }
 
 public String removeBadItems(String in)
 {
  String result = in;
  
  for (String bad : getTranslationRules().getDelete())
  {
   result = result.replaceAll(bad,"");
  }
  
  return(result);
 }
 
 public String replaceBadItems(String in)
 {
  String result = in;

  for (Map.Entry<String,String> rep : getTranslationRules().getReplace().entrySet())
  {
   result = result.replaceAll(rep.getKey(),rep.getValue());
  }
  
  return(result);
 }
 
 public String replaceBadItemsManyTimes(String in)
 {
  String result = in;
  String exResult = "";
  
  do
  {
   exResult = result;
   
   for (Map.Entry<String,String> rep : getTranslationRules().getReplaceMany().entrySet())
   {
    result = result.replaceAll(rep.getKey(),rep.getValue());
   }
  }
  while (!result.equals(exResult));
  
  return(result);
 }
 
 public String translate(String in)
 {
  return(replaceBadItemsManyTimes(replaceBadItems(removeBadItems(deleteLinesStartingWithBadItems(in)))).trim().replaceAll("[\\t ]+\n","\n"));
 }
 
 public void translate(String inPath, String outPath) throws IOException
 {
  Files.string2File(translate(Files.file2String(inPath)), outPath);
 }
 
 public static void main(String args[]) throws IOException
 {
  System.out.println(new TranslationAgent(new MySQL2Oracle("mshj",true)).translate(Files.file2String(args[0])));
 }
}

The String translate(String in) return the translation of its String input by calling successively deleteLinesStartingWithBadItems which deletes or comments the non-needed lines, removeBadItems which removes non-needed tokens, replaceBadItems  which replaces tokens by their translations and finally replaceBadItemsManyTimes which perform unbounded number of replacement iterations.
The replaceBadItemsManyTimes performs as many replacement iterations as there should be, for example when translating inner foreign key index declarations in a MySQL script: each index will be detected in one iteration and thus there will be as many iterations as such index declarations.

The main method displays the result of the translation (from MySQL to Oracle SQL dialect) of the script at the path args[0].

(+) What is supported:

- Regular insert into, delete, drop, create, update, alter, select ... instructions
- Constraints (foreign keys and uniqueness)
- Indexes

(-) What is not supported:

- Granting privileges
- Creating users and schema

Off course all this can be improved. The aim was to rapidly create an incomplete yet useful translator using only replacement and Java Regular Expressions. A more complete solution could be achieved by using parsers (JavaCC).


Friday, June 7, 2013

Moving, Renaming Directories,Files or Packages in Java

It is often needed to move and/or rename directories using java programs. One particular case where i needed this feature was after following some tutorial on the web ending up with a heavily configured Eclipse project and where i needed to rename the hole project and sometimes even package names to fit my desired parameters in stead of starting the configuration all over again for a new project. (You may be interested if you followed this JEE tutorial)
Eclipse on its side proposes Project re-factoring but for packages renaming you'll need to do it by hand.
Another scenario where this feature maybe useful is when you do some code generation and where you need to create a new project instance starting from some skeleton.

Classes are grouped in an Eclipse Java Project illustrated by this tree:



1. Package file.util.files:

This package contains an abstract class that exposes useful operations for files.

FileUtils.java:


package file.util.files;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public abstract class FileUtils
{
 public static String LINE_SEPARATOR = System.getProperty("line.separator");
 public static String FILE_SEPARATOR = System.getProperty("file.separator");
 
 public static String toPath(String packageName)
 {
  return(packageName.replaceAll("\\.", FILE_SEPARATOR));
 }
 
 public static String getExtension(File file)
 {
  String result = "";
  
  if (file.isFile())
  {
   String fileName = file.getName();
   int lastDotIndex = fileName.lastIndexOf(".");
   
   if ((lastDotIndex != -1) && (lastDotIndex != (fileName.length()-1)))
   {
    result = fileName.substring(lastDotIndex + 1);
   }
  }
  
  return(result);
 }
 
 public static void string2File(String s, String path) throws IOException 
 {
  FileWriter fw = new FileWriter(path);
  BufferedWriter bw = new BufferedWriter(fw);
  bw.write(s);
  bw.close();
  fw.close();
 }
 
 public static String file2String(String filename) throws IOException 
 {
  FileReader fr = new FileReader(filename);
  BufferedReader br = new BufferedReader(fr);
  StringBuilder result = new StringBuilder();
  String line;
  
  while ((line = br.readLine()) != null) 
  {
   result.append(line);
   result.append(LINE_SEPARATOR);
  }
  
  br.close();
  fr.close();
  
  return result.toString();
 }
 
 public static void copyFolder(File src, File dest) throws IOException
 {
  if(src.isDirectory())
  {
   if(!dest.exists())
   {
    dest.mkdir();
   }
   String files[] = src.list();
    
   for (String file : files)
   {
    File srcFile = new File(src, file);
    File destFile = new File(dest, file);
    copyFolder(srcFile,destFile);
   }
  }
  else
  {
   InputStream in = new FileInputStream(src);
   OutputStream out = new FileOutputStream(dest); 
        
   byte[] buffer = new byte[1024];
    
   int length;
   while ((length = in.read(buffer)) > 0)
   {
    out.write(buffer, 0, length);
   }
    
   in.close();
   out.close();
  }
 }
 
 public static void removeEmptySubDirs(File source)
 {
  if (source.isDirectory())
  {
   if (!source.delete())
   {
    for (File sub : source.listFiles())
    {
     removeEmptySubDirs(sub);
    }
   }
  }
 }
}

public static String toPath(String packageName) in lines 19-22:
Replaces a package name to its corresponding path. Example: file.util.files will return file/util/files for Linux and file\util\files for Windows.

public static String getExtension(File file) in lines 24-40:
Returns the extension (the sub-string of its name starting from the last . occurrence excluding the .) of the File file.

public static void string2File(String s, String path) throws IOException in lines 42-49:
This static method takes as parameters a String s and a String representing a file system valid path path. It stores the content of s in the file at the path path.

public static String file2String(String filename) throws IOException in lines 551-68:
This static method returns the content of the file at the path filename as a String.

public static void copyFolder(File src, File dest) throws IOException in lines 70-103:
Copy the directory (and all its sub-directories) or the file described by the File src to the the directory or file described by the File dest.

public static void removeEmptySubDirs(File source) in lines 105-118:
Removes all the empty sub-directories of the directory described by the File source.

2. Package file.util.files.refactor:

This package contains the master class that will expose he re-factoring feature and a configuration class holding the re-factoring parameters.

RefactorConfiguration.java:


package file.util.files.refactor;

import java.util.ArrayList;
import java.util.Arrays;

public class RefactorConfiguration
{
 public static String[] DEFAULT_ECLIPSE_EXTENSIONS_2_SKIP = {"jpeg","jpg","bmp","png","gif","zip","jar","war","class","pdf","ttf","css"};
 
 public static RefactorConfiguration DEFAULT_ECLIPSE_REFACTOR_CONFIGURATION = 
   new RefactorConfiguration
   (
     true,
     false,
     true,
     false,
     true,
     true,
     new ArrayList<String>(Arrays.asList(DEFAULT_ECLIPSE_EXTENSIONS_2_SKIP)),
     false,
     new ArrayList<String>(),
     true
   );
 
 private boolean inDirNames;
 public boolean isInDirNames() {
  return inDirNames;
 }
 public void setInDirNames(boolean inDirNames) {
  this.inDirNames = inDirNames;
 }
 
 private boolean exactMatchInDirNames;
 public boolean isExactMatchInDirNames() {
  return exactMatchInDirNames;
 }
 public void setExactMatchInDirNames(boolean exactMatchInDirNames) {
  this.exactMatchInDirNames = exactMatchInDirNames;
 }

 private boolean inFileNames;
 public boolean isInFileNames() {
  return inFileNames;
 }
 public void setInFileNames(boolean inFileNames) {
  this.inFileNames = inFileNames;
 }
 
 private boolean exactMatchInFileNames;
 public boolean isExactMatchInFileNames() {
  return exactMatchInFileNames;
 }
 public void setExactMatchInFileNames(boolean exactMatchInFileNames) {
  this.exactMatchInFileNames = exactMatchInFileNames;
 }

 private boolean inFiles;
 public boolean isInFiles() {
  return inFiles;
 }
 public void setInFiles(boolean inFiles) {
  this.inFiles = inFiles;
 }
 
 private boolean skipExtensions;
 public boolean isSkipExtensions() {
  return skipExtensions;
 }
 public void setSkipExtensions(boolean skipExtensions) {
  this.skipExtensions = skipExtensions;
 }
 
 private ArrayList<String> extensions2Skip;
 public ArrayList<String> getExtensions2Skip() {
  return extensions2Skip;
 }
 public void setExtensions2Skip(ArrayList<String> extensions2Skip) {
  this.extensions2Skip = extensions2Skip;
 }
 
 private boolean visitExtensions;
 public boolean isVisitExtensions() {
  return visitExtensions;
 }
 public void setVisitExtensions(boolean visitExtensions) {
  this.visitExtensions = visitExtensions;
 }
 
 private ArrayList<String> extensions2Visit;
 public ArrayList<String> getExtensions2Visit() {
  return extensions2Visit;
 }
 public void setExtensions2Visit(ArrayList<String> extensions2Visit) {
  this.extensions2Visit = extensions2Visit;
 }
 
 private boolean packageNamesAware;
 public boolean isPackageNamesAware() {
  return packageNamesAware;
 }
 public void setPackageNamesAware(boolean packageNamesAware) {
  this.packageNamesAware = packageNamesAware;
 }
 
 public RefactorConfiguration
  (
   boolean inDirNames,
   boolean exactMatchInDirNames,
   boolean inFileNames,
   boolean exactMatchInFileNames,
   boolean inFiles,
   boolean skipExtensions,
   ArrayList<String> extensions2Skip,
   boolean visitExtensions,
   ArrayList<String> extensions2Visit,
   boolean packageNamesAware
  )
 {
  this.inDirNames = inDirNames;
  this.exactMatchInDirNames = exactMatchInDirNames;
  this.inFileNames = inFileNames;
  this.exactMatchInFileNames = exactMatchInFileNames;
  this.inFiles = inFiles;
  this.skipExtensions = skipExtensions;
  this.extensions2Skip = extensions2Skip;
  this.visitExtensions = visitExtensions;
  this.extensions2Visit = extensions2Visit;
  this.packageNamesAware = packageNamesAware;
 }
}

The class defines several boolean flags having the following meanings:
- inDirNames: if set to true re-factoring will also address the directories' names.
- exactMatchInDirNames: if set to true (and if inDirNames is set to true) re-factoring will address directories with names matching exactly the pattern to be re-factored.
inFileNames: if set to true re-factoring will also address the files' names.
exactMatchInFileNames: if set to true (and if inFileNames is set to true) re-factoring will address files' names matching exactly the pattern to be re-factored.
- inFiles: if set to true re-factoring will also address the content of files.
- skipExtensions: if set to true all the files having extensions in extensions2Skip will be skipped.
- visitExtensions: if set to true only files having extension in extensions2Visit will be addressed by re-factoring.
Using skipExtensions and visitExtensions is mutually exclusive, only one should be set to true otherwise all files will not be handled.
- packageNamesAware: is set to true and if for example we are replacing each occurrence of file.util.files.refactor by test.refactor then each occurrence of the former will be replaced by the latter in the content of every visited file but also every directory having in its path the sub-path expression file/util/files/refactor, lets say the directory is $Prefix/file/util/files/refactor/$Suffix will be moved to the path $Prefix/test/refactor/$Suffix.

In lines 8-23, a good configuration for re-factoring an eclipse project is provided.

RefactorAgent.java:



package file.util.files.refactor;

import java.io.File;
import java.io.IOException;
import file.util.files.FileUtils;

public class RefactorAgent
{
 private RefactorConfiguration configuration;
 public RefactorConfiguration getConfiguration() {
  return configuration;
 }
 public void setConfiguration(RefactorConfiguration configuration) {
  this.configuration = configuration;
 }
 
 public RefactorAgent(RefactorConfiguration configuration)
 {
  this.configuration = configuration;
 }
 
 public RefactorAgent ()
 {
  this.configuration = RefactorConfiguration.DEFAULT_ECLIPSE_REFACTOR_CONFIGURATION;
 }
 
 public void refactor(File mySource, String myFrom, String myTo) throws IOException
 {
  if (mySource.exists())
  {
   File newSource = mySource;
   
   if (mySource.isDirectory() && getConfiguration().isInDirNames())
   {
    if (
      getConfiguration().isPackageNamesAware()
      &&
      (
       (getConfiguration().isExactMatchInDirNames() && mySource.getAbsolutePath().endsWith(FileUtils.toPath(myFrom)))
       ||
       (!getConfiguration().isExactMatchInDirNames() && (mySource.getAbsolutePath().indexOf(FileUtils.toPath(myFrom)) != -1))
      )
     )
    {
     newSource = new File (mySource.getAbsolutePath().replaceAll(FileUtils.toPath(myFrom), FileUtils.toPath(myTo)));
     newSource.mkdirs();
     mySource.renameTo(newSource);
    }
    
    if (
      !getConfiguration().isPackageNamesAware()
      &&
      (
       (getConfiguration().isExactMatchInDirNames() && mySource.getName().equals(myFrom))
       ||
       (!getConfiguration().isExactMatchInDirNames() && (mySource.getName().indexOf(myFrom) != -1))
      )
     )
    {
     newSource = new File (mySource.getParent() + FileUtils.FILE_SEPARATOR + mySource.getName().replaceAll(myFrom, myTo));
     newSource.mkdirs();
     mySource.renameTo(newSource);
    }
   }
   
   if (mySource.isFile() && getConfiguration().isInFileNames())
   {
    if (
      (getConfiguration().isExactMatchInFileNames() && mySource.getName().equals(myFrom))
      ||
      (!getConfiguration().isExactMatchInFileNames() && (mySource.getName().indexOf(myFrom) != -1))
     )
    {
     newSource = new File (mySource.getParent() + FileUtils.FILE_SEPARATOR + mySource.getName().replaceAll(myFrom, myTo));
     mySource.renameTo(newSource);
    }
   }
   
   if (newSource.isDirectory())
   {
    for (File sub : newSource.listFiles())
    {
     refactor(sub, myFrom, myTo);
    }
   }
   else
   {
    if (getConfiguration().isInFiles())
    {
     String extension = FileUtils.getExtension(newSource);
     
     if (
       (!getConfiguration().isSkipExtensions() && getConfiguration().isVisitExtensions() && getConfiguration().getExtensions2Visit().contains(extension)) 
       || 
       (!getConfiguration().isVisitExtensions() && getConfiguration().isSkipExtensions() && !getConfiguration().getExtensions2Skip().contains(extension))
      )
     {
      FileUtils.string2File(FileUtils.file2String(newSource.getPath()).replaceAll(myFrom, myTo).replaceAll(FileUtils.toPath(myFrom), FileUtils.toPath(myTo)),newSource.getPath());
     }
    }
   }
  }
 }
 
 public static void main(String[] args) throws IOException
 {
  new RefactorAgent().refactor(new File("/some_path/Test"),"Test","NewName");
  new RefactorAgent().refactor(new File("/some_path/newName"),"mshj.tutorial","pckgname.spckgname1.spckgname2");
  FileUtils.removeEmptySubDirs(new File("/some_path/newName"));
 }
}

This is the master class.
public void refactor(File mySource, String myFrom, String myTo) throws IOException in lines 27-103:
Performs the re-factoring on the file or directory described by the File mySource replacing occurrences of myFrom by myTo.

The main method is provided with the code to rename the project (and its packages) obtained in this JEE tutorial to custom values.


Execute Oracle/MySQL SQL Scripts in Java

Sometimes it is useful to execute SQL scripts from Java programs. In the following we present java classes permitting to:

- Execute Oracle or MySQL SQL update scripts given a valid and authorized DB connection.
-  Execute Oracle or MySQL SQL query and collect the results as a collection of lines (where a line is a Collection of Strings) or as a simple HTML page with a table summarizing the results.

The classes are grouped in an Eclipse Java Project illustrated by the following tree:



1. Project dependencies:

The project needs jdbc libraries for both Oracle (download here) and MySQL (download here) databases.

2. Project Packages:

The project defines two packages:

sql.util.connection: which contains classes defining JDBC Oracle and MySQL database connections.

ConnectionDefinition.java: is an abstract class describing general JDBC database connection as follows:

package sql.util.connection;

public abstract class ConnectionDefinition
{
 private String url;  
 private String user;  
 private String password;
    
 public String getUrl() {
  return url;
 }
 public void setUrl(String url) {
  this.url = url;
 }
 
 public String getUser() {
  return user;
 }
 public void setUser(String user) {
  this.user = user;
 }
 
 public String getPassword() {
  return password;
 }
 public void setPassword(String password) {
  this.password = password;
 }
 
 public ConnectionDefinition(String url, String user, String password)
 {
  this.url = url;
  this.user = user;
  this.password = password;
 }
 
 public static String display(ConnectionDefinition cd)
 {
  if (cd == null)
  {
   return("ConnectionDefinition :\n\tnull");
  }
  else
  {
   return( "ConnectionDefinition :\n"
     + "\turl = " + ((cd.getUrl() == null)?"null":cd.getUrl()) + "\n"
     + "\tuser = " + ((cd.getUser() == null)?"null":cd.getUser()) + "\n"
     + "\tpassword = " + ((cd.getPassword() == null)?"null":cd.getPassword()) + "\n");
  }
 }
}

The class defines three properties (the DB connection url, the user name and the corresponding password) and a display function returning a friendly String description of the connection. This class is extended by :

OracleConnectionDefinition.java:

package sql.util.connection;

public class OracleConnectionDefinition extends ConnectionDefinition
{
 private String host;
 public String getHost() {
  return host;
 }
 public void setHost(String host) {
  this.host = host;
 }
 
 private int port;
 public int getPort() {
  return port;
 }
 public void setPort(int port) {
  this.port = port;
 }
 
 private String sid;
 public String getSid() {
  return sid;
 }
 public void setSid(String sid) {
  this.sid = sid;
 }
 
 public OracleConnectionDefinition(String host, int port, String sid, String user, String password)
 {
  super("jdbc:oracle:thin:@" + host + ":" + port + ":" + sid, user, password);
 }
 
 public OracleConnectionDefinition(String sid, String user, String password)
 {
  super("jdbc:oracle:thin:@localhost:1521:" + sid, user, password);
 }
 
 public OracleConnectionDefinition(String user, String password)
 {
  super("jdbc:oracle:thin:@localhost:1521:xe", user, password);
 }
}

For Oracle databases the JDBC connection url is formed by three parameters the host (the server address), the listening port and the sid (site identifier). The class provides several default constructors (1521 is the default listening port and xe is the default sid for Oracle Express Editions).

MySQLConnectionDefinition.java:

package sql.util.connection;

public class MySQLConnectionDefinition extends ConnectionDefinition
{
 private String host;
 public String getHost() {
  return host;
 }
 public void setHost(String host) {
  this.host = host;
 }
 
 private int port;
 public int getPort() {
  return port;
 }
 public void setPort(int port) {
  this.port = port;
 }
 
 private String schema;
 public String getSchema() {
  return schema;
 }
 public void setSchema(String schema) {
  this.schema = schema;
 }
 
 public MySQLConnectionDefinition(String host, int port, String schema, String user, String password)
 {
  super("jdbc:mysql://" + host + ":" + port + "/" + schema, user, password);
 }
 
 public MySQLConnectionDefinition(String schema, String user, String password)
 {
  super("jdbc:mysql://localhost:3306/" + schema, user, password);
 }
 
 public MySQLConnectionDefinition(String user, String password)
 {
  super("jdbc:mysql://localhost:3306/mysql", user, password);
 }
}

For MySQL databases the JDBC connection url is formed by three parameters the host (the server address), the listening port and the schema. The class provides several default constructors (3306 is the default listening port and mysql is the default schema).


sql.util: which contains the class that will execute scripts.

SQLExecutor.java:

package sql.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import sql.util.connection.ConnectionDefinition;
import sql.util.connection.MySQLConnectionDefinition;
import sql.util.connection.OracleConnectionDefinition;

public class SQLExecutor
{
 public static String LINE_SEPARATOR = System.getProperty("line.separator");
 private static final String ORACLE_DRIVER_NAME = "oracle.jdbc.OracleDriver";
 private static final String MYSQL_DRIVER_NAME = "com.mysql.jdbc.Driver";
 
 public static void loadMySQLDriver()
 {
  try
  {
   Class.forName(MYSQL_DRIVER_NAME);
  }
  catch(ClassNotFoundException e)
  {
   System.out.println(MYSQL_DRIVER_NAME + " is missing.");
   e.printStackTrace();
  }
 }
 
 public static void loadOracleDriver()
 {
  try
  {
   Class.forName(ORACLE_DRIVER_NAME);
  }
  catch(ClassNotFoundException e)
  {
   System.out.println(ORACLE_DRIVER_NAME + " is missing.");
   e.printStackTrace();
  }
 }
 
 ConnectionDefinition connection;
 public ConnectionDefinition getConnection() {
  return connection;
 }
 public void setConnection(ConnectionDefinition connection) {
  this.connection = connection;
 }
 
 public SQLExecutor(ConnectionDefinition connection)
 {
  this.connection = connection;
  
  if (connection instanceof OracleConnectionDefinition)
  {
   loadOracleDriver();
  }
  
  if (connection instanceof MySQLConnectionDefinition)
  {
   loadMySQLDriver();
  }
 }
 
 public void executeUpdate(String query)
 {
  Connection con;
  Statement stmt;
  
  try
  {
   con = DriverManager.getConnection(getConnection().getUrl(),getConnection().getUser(),getConnection().getPassword());
   
   try
   {
    stmt = con.createStatement();
    
    query = query.trim();
    
    if (query.endsWith(";"))
    {
     query = query.substring(0,query.length()-1);
    }
    
    try
    {
     stmt.executeUpdate(query);
    }
    catch (SQLException e)
    {
     System.out.println("Could not execute update query:\n" + query);
     e.printStackTrace();
    }
    
    stmt.close();
    stmt = null;
    
   }
   catch (SQLException e)
   {
    System.out.println("Could not create statement.");
    e.printStackTrace();
   }
   
   con.close();
         con = null;
   
  }
  catch (SQLException e)
  {
   System.out.println("Something is wrong with your connection parameters:\n" + ConnectionDefinition.display(getConnection()));
   e.printStackTrace();
  }
 }
 
 public void executeScript(String script)
 {
  Connection con;
  Statement stmt;
  
  try
  {
   con = DriverManager.getConnection(getConnection().getUrl(),getConnection().getUser(),getConnection().getPassword());
   
   try
   {
    stmt = con.createStatement();
    
    script = script.trim();
    
    if (script.endsWith(";"))
    {
     script = script.substring(0,script.length()-1);
    }
    
    String[] queries = script.split(";" + LINE_SEPARATOR);
    
    int counter = 0;
    for (String query : queries)
    {
     counter++;
     if ((query != null) && !query.isEmpty())
     {
      try
      {
       stmt.executeUpdate(query);
      }
      catch (SQLException e)
      {
       System.out.println("Could not execute update query in line " + counter + ":\n" + query);
       e.printStackTrace();
      }
     }
    }
    
    stmt.close();
    stmt = null;
   }
   catch (SQLException e)
   {
    System.out.println("Could not create statement.");
    e.printStackTrace();
   }
   
   con.close();
         con = null;
   
  }
  catch (SQLException e)
  {
   System.out.println("Something is wrong with your connection parameters:\n" + ConnectionDefinition.display(getConnection()));
   e.printStackTrace();
  } 
 }
 
 public ArrayList<ArrayList<String>> executeQueryArray(String query)
 {
  ArrayList<ArrayList<String>> result = new ArrayList<ArrayList<String>>();
  
  Connection con;
  Statement stmt;
  
  try
  {
   con = DriverManager.getConnection(getConnection().getUrl(),getConnection().getUser(),getConnection().getPassword());
   
   try
   {
    stmt = con.createStatement();
    
    query = query.trim();
    
    if (query.endsWith(";"))
    {
     query = query.substring(0,query.length()-1);
    }
    
    ResultSet resultSet;
    
    try
    {
     resultSet = stmt.executeQuery(query);
     
     ResultSetMetaData rsmd;
     
     ArrayList<String> columns = new ArrayList<String>();
     
     try
     {
      rsmd = resultSet.getMetaData();
      
      int counter = 0;
      
      while (counter < rsmd.getColumnCount())
      {
       columns.add(rsmd.getColumnLabel(counter+1));
       counter++;
      }
      
      result.add(columns);
      
      while (resultSet.next())
      {
       ArrayList<String> line = new ArrayList<String>();
       int counter2 = 0;
       
       while (counter2 < columns.size())
       {
        line.add(resultSet.getString(counter2+1));
        counter2++;
       }
       
       result.add(line);
      }
      
     }
     catch (SQLException e)
     {
      System.out.println("Could not get MetaData from ResultSet for query:\n" + query);
      e.printStackTrace();
     }
     
     resultSet.close();
    }
    catch (SQLException e)
    {
     System.out.println("Could not execute query:\n" + query);
     e.printStackTrace();
    }
    
    stmt.close();
    stmt = null;
    
   }
   catch (SQLException e)
   {
    System.out.println("Could not create statement.");
    e.printStackTrace();
   }
   
  }
  catch (SQLException e)
  {
   System.out.println("Something is wrong with your connection parameters:\n" + ConnectionDefinition.display(getConnection()));
   e.printStackTrace();
  }
  
  return(result);
 }
 
 public String executeQuery2HTML(String query)
 {
  String finalResult = "<html>" + LINE_SEPARATOR + "\t</head>" + LINE_SEPARATOR + "\t<body>" + LINE_SEPARATOR + "\t\t<table border = \"1\" align = \"center\">";
  
  boolean done = false;
  
  for (ArrayList<String> array : executeQueryArray(query))
  {
   finalResult += LINE_SEPARATOR + "\t\t\t<tr>" + LINE_SEPARATOR;
   for (String cell : array)
   {
    finalResult += ((done)?"\t\t\t\t<td align = \"center\">":"\t\t\t\t<th align = \"center\">") + cell + ((done)?"</td>" + LINE_SEPARATOR:"</th>" + LINE_SEPARATOR);
   }
   finalResult += "\t\t\t</tr>";
   
   done = true;
  }
  
  return(finalResult + LINE_SEPARATOR + "\t\t</table>" + LINE_SEPARATOR + "\t</body>" + LINE_SEPARATOR + "</html>");
 }
 
 public static void testOracle(String user, String password)
 {
  String scriptOracle = "CREATE TABLE test (id NUMERIC(2) NOT NULL, title VARCHAR2(10), description VARCHAR2(20), PRIMARY KEY(id));" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(1,'title1','Title One');" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(2,'title2','Title Two');" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(3,'title3','Title Three');" + LINE_SEPARATOR
       + "COMMIT;";
  
  SQLExecutor oracleSQLExecutor = new SQLExecutor(new OracleConnectionDefinition(user,password));
  
  oracleSQLExecutor.executeScript(scriptOracle);
  System.out.println(oracleSQLExecutor.executeQuery2HTML("SELECT * FROM test;"));
  
  String scriptCleanUp = "DROP TABLE test;";
     oracleSQLExecutor.executeScript(scriptCleanUp);
 }
 
 public static void testMySQL(String user, String password)
 {
  String scriptMySQL = "CREATE TABLE test (id NUMERIC(2) NOT NULL, title VARCHAR(10), description VARCHAR(20), PRIMARY KEY(id));" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(1,'title1','Title One');" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(2,'title2','Title Two');" + LINE_SEPARATOR
       + "INSERT INTO test VALUES(3,'title3','Title Three');" + LINE_SEPARATOR
       + "COMMIT;";
  
  SQLExecutor mySQLExecutor = new SQLExecutor(new MySQLConnectionDefinition(user,password));
  
  mySQLExecutor.executeScript(scriptMySQL);
     System.out.println(mySQLExecutor.executeQuery2HTML("SELECT * FROM test;"));
  
     String scriptCleanUp = "DROP TABLE test;";
     mySQLExecutor.executeScript(scriptCleanUp);
 }
 
 public static void main(String[] args)
    {
     if (args.length == 3)
     {
      if ("oracle".equals(args[0].toLowerCase()))
      {
       testOracle(args[1],args[2]);
      }
      
      if ("mysql".equals(args[0].toLowerCase()))
      {
       testMySQL(args[1],args[2]);
      }
     }
     else
     {
      System.out.println("Executes test scripts on \"localhost:1521:xe\" or \"localhost:3306/mysql\"\n\nArguments:\n- DB Type : Oracle or MySQL\n- DB User Name\n- DB Password\n\nExample: \"oracle\" \"system\" \"manager\"");
     }
    }
}

We briefly describe here the important methods:

public void executeScript(String script) in lines 120-178:
The method splits the script into several single instructions (statements) then executes the instructions one by one. Exceptions are caught whenever the connection does not succeed, the statement execution is not possible (SQL syntax error or maybe not enough privileges to execute the statement).

public ArrayList<ArrayList<String>> executeQueryArray(String query) in lines 180-273:
The method returns the result of the execution of a select query as a list of lists of Strings. The first inner list contains the returned field titles and each subsequent inner list (if any) contains values for those fields. Again, exceptions are caught whenever the connection does not succeed, the statement execution is not possible (SQL syntax error or maybe not enough privileges to execute the statement).

public String executeQuery2HTML(String query) in lines 275-294:
Relies on the previous method to format the result of the execution of a select query as a table in a simple html page.

public static void testOracle(String user, String password) in lines 296-311 and public static void testMySQL(String user, String password) in lines 313-328:
These two methods use the previously described methods to execute test scripts and queries for simple default settings for Oracle and MySQL databases. A dummy test table is created and three lines are inserted into it. A select statement is executed against the test table and corresponding results are displayed as html. Finally the test table is dropped.

These two methods can be used by executing the main method of the class with three arguments: the database type, the authorized user name and password.