From 1e709c334434c68386bb88dc29e79101fcf5824f Mon Sep 17 00:00:00 2001
From: tobias <tobias@5a81b35b-ba03-0410-adc8-b2c5c5119f08>
Date: Tue, 11 May 2010 13:53:58 +0000
Subject: [PATCH] - added unit tests for sdf file reader/writer - moved
 compare_files to base/test_utils - reorganized sdf file reader/writer

git-svn-id: https://dng.biozentrum.unibas.ch/svn/openstructure/trunk@2236 5a81b35b-ba03-0410-adc8-b2c5c5119f08
---
 modules/base/src/CMakeLists.txt               |   3 +-
 modules/base/src/test_utils/compare_files.cc  |  52 +++
 modules/base/src/test_utils/compare_files.hh  |  27 ++
 modules/gfx/tests/CMakeLists.txt              |   6 +-
 modules/gfx/tests/test_ent_pov_export.cc      |  31 +-
 modules/io/src/mol/CMakeLists.txt             |   4 +
 modules/io/src/mol/entity_io_sdf_handler.cc   | 378 +-----------------
 modules/io/src/mol/entity_io_sdf_handler.hh   |  24 --
 modules/io/src/mol/sdf_reader.cc              | 275 +++++++++++++
 modules/io/src/mol/sdf_reader.hh              |  67 ++++
 modules/io/src/mol/sdf_writer.cc              | 158 ++++++++
 modules/io/src/mol/sdf_writer.hh              |  61 +++
 modules/io/tests/test_io_pdb.cc               |  31 +-
 modules/io/tests/test_io_sdf.cc               | 182 ++++++++-
 .../{test_in.sdf => sdf/compound.sdf}         | 264 ++++++------
 .../tests/testfiles/sdf/empty_dataheader.sdf  |  48 +++
 modules/io/tests/testfiles/sdf/multiple.sdf   |  72 ++++
 modules/io/tests/testfiles/sdf/properties.sdf |  48 +++
 modules/io/tests/testfiles/sdf/simple.sdf     |  18 +
 .../tests/testfiles/sdf/wrong_atomcount.sdf   |  18 +
 .../testfiles/sdf/wrong_atomlinelength.sdf    |  18 +
 .../io/tests/testfiles/sdf/wrong_atompos.sdf  |  18 +
 .../testfiles/sdf/wrong_bondatomnumber.sdf    |  18 +
 .../tests/testfiles/sdf/wrong_bondcount.sdf   |  18 +
 .../testfiles/sdf/wrong_bondlinelength.sdf    |  18 +
 .../io/tests/testfiles/sdf/wrong_bondtype.sdf |  18 +
 .../io/tests/testfiles/sdf/wrong_charge.sdf   |  18 +
 .../tests/testfiles/sdf/wrong_dataheader.sdf  |  48 +++
 28 files changed, 1352 insertions(+), 589 deletions(-)
 create mode 100644 modules/base/src/test_utils/compare_files.cc
 create mode 100644 modules/base/src/test_utils/compare_files.hh
 create mode 100644 modules/io/src/mol/sdf_reader.cc
 create mode 100644 modules/io/src/mol/sdf_reader.hh
 create mode 100644 modules/io/src/mol/sdf_writer.cc
 create mode 100644 modules/io/src/mol/sdf_writer.hh
 rename modules/io/tests/testfiles/{test_in.sdf => sdf/compound.sdf} (97%)
 create mode 100644 modules/io/tests/testfiles/sdf/empty_dataheader.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/multiple.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/properties.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/simple.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_atomcount.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_atomlinelength.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_atompos.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_bondatomnumber.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_bondcount.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_bondlinelength.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_bondtype.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_charge.sdf
 create mode 100644 modules/io/tests/testfiles/sdf/wrong_dataheader.sdf

diff --git a/modules/base/src/CMakeLists.txt b/modules/base/src/CMakeLists.txt
index ecdc389db..c2d698cf9 100644
--- a/modules/base/src/CMakeLists.txt
+++ b/modules/base/src/CMakeLists.txt
@@ -8,6 +8,7 @@ units.cc
 string_ref.cc
 platform.cc
 message.cc
+test_utils/compare_files.cc
 )
 
 set(OST_BASE_HEADERS
@@ -32,7 +33,7 @@ tri_matrix.hh
 )
 
 module(NAME base SOURCES ${OST_BASE_SOURCES} 
-       HEADERS generic_property_def.hh IN_DIR export_helper ${OST_BASE_HEADERS}
+       HEADERS generic_property_def.hh IN_DIR export_helper compare_files.hh IN_DIR test_utils ${OST_BASE_HEADERS}
        DEPENDS_ON geom
        HEADER_OUTPUT_DIR ost)
 
diff --git a/modules/base/src/test_utils/compare_files.cc b/modules/base/src/test_utils/compare_files.cc
new file mode 100644
index 000000000..ff5578299
--- /dev/null
+++ b/modules/base/src/test_utils/compare_files.cc
@@ -0,0 +1,52 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+
+#include <iostream>
+#include <fstream>
+#include "compare_files.hh"
+
+bool compare_files(const String& test, const String& gold_standard)
+{
+  std::ifstream test_stream(test.c_str());
+  std::ifstream gold_stream(gold_standard.c_str());
+  String test_line, gold_line;
+  while (true) {
+    bool test_end=std::getline(test_stream, test_line);
+    bool gold_end=std::getline(gold_stream, gold_line);
+    if (!(test_end || gold_end)) {
+      return true;
+    }
+    if (!test_end) {
+      std::cerr << gold_standard << " contains additional line(s):"
+                << std::endl << gold_line;
+      return false;
+    }
+    if (!gold_end) {
+      std::cerr << test << " contains additional line(s):"
+                << std::endl << test_line;
+      return false;
+    }
+    if (gold_line!=test_line) {
+      std::cerr << "line mismatch:" << std::endl << "test: " << test_line
+                << std::endl << "gold: " << gold_line;
+      return false;
+    }
+  }
+  return true;
+}
diff --git a/modules/base/src/test_utils/compare_files.hh b/modules/base/src/test_utils/compare_files.hh
new file mode 100644
index 000000000..7f441ae69
--- /dev/null
+++ b/modules/base/src/test_utils/compare_files.hh
@@ -0,0 +1,27 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+
+#ifndef OST_COMPARE_FILES_HH
+#define OST_COMPARE_FILES_HH
+
+#include <ost/base.hh>
+
+bool compare_files(const String& test, const String& gold_standard);
+
+#endif
diff --git a/modules/gfx/tests/CMakeLists.txt b/modules/gfx/tests/CMakeLists.txt
index a055e099e..fe6a3d670 100644
--- a/modules/gfx/tests/CMakeLists.txt
+++ b/modules/gfx/tests/CMakeLists.txt
@@ -4,10 +4,8 @@ set(OST_GFX_UNIT_TESTS
 )
 if (ENABLE_IMG)
   list(APPEND OST_GFX_UNIT_TESTS test_map_octree.cc)
-
-	ost_unittest(gfx "${OST_GFX_UNIT_TESTS}")
-
-	target_link_libraries(gfx_tests ost_io)
 endif()
 
+ost_unittest(gfx "${OST_GFX_UNIT_TESTS}")
 
+target_link_libraries(gfx_tests ost_io)
diff --git a/modules/gfx/tests/test_ent_pov_export.cc b/modules/gfx/tests/test_ent_pov_export.cc
index 450051dc7..bfb72ec79 100644
--- a/modules/gfx/tests/test_ent_pov_export.cc
+++ b/modules/gfx/tests/test_ent_pov_export.cc
@@ -26,6 +26,7 @@
 #include <ost/io/load_entity.hh>
 #include <ost/gfx/scene.hh>
 #include <ost/gfx/entity.hh>
+#include <ost/test_utils/compare_files.hh>
 
 using boost::unit_test_framework::test_suite;
 using namespace ost;
@@ -42,36 +43,6 @@ boost::shared_ptr<Entity> prepare_object(gfx::RenderMode::Type mode)
   return gfx_ent;
 }
 
-bool compare_files(const String& test, const String& gold_standard)
-{
-  std::ifstream test_stream(test.c_str());
-  std::ifstream gold_stream(gold_standard.c_str());
-  String test_line, gold_line;
-  while (true) {
-    bool test_end=std::getline(test_stream, test_line);
-    bool gold_end=std::getline(gold_stream, gold_line);
-    if (!(test_end || gold_end)) {
-      return true;
-    }
-    if (!test_end) {
-      std::cerr << gold_standard << " contains additional line(s):"
-                << std::endl << gold_line;
-      return false;
-    }
-    if (!gold_end) {
-      std::cerr << test << " contains additional line(s):"
-                << std::endl << test_line;
-      return false;
-    }
-    if (gold_line!=test_line) {
-      std::cerr << "line mismatch:" << std::endl << "test: " << test_line 
-                << std::endl << "gold: " << gold_line;
-      return false;
-    }
-  }
-  return true;
-}
-
 // The GfxView uses a std::map for efficient access to atoms. This however has 
 // implications for the POV export. In general we can't assume that the atoms 
 // are written in any particular order. That's why we first filter out all 
diff --git a/modules/io/src/mol/CMakeLists.txt b/modules/io/src/mol/CMakeLists.txt
index 5cdf0462b..860b4df0d 100644
--- a/modules/io/src/mol/CMakeLists.txt
+++ b/modules/io/src/mol/CMakeLists.txt
@@ -6,6 +6,8 @@ entity_io_pdb_handler.cc
 pdb_io.cc
 pdb_writer.cc
 entity_io_sdf_handler.cc	
+sdf_reader.cc
+sdf_writer.cc
 save_entity.cc
 load_entity.cc
 surface_io_msms_handler.cc
@@ -26,6 +28,8 @@ pdb_reader.hh
 entity_io_pdb_handler.hh	
 pdb_writer.hh
 entity_io_sdf_handler.hh	
+sdf_reader.hh
+sdf_writer.hh
 save_entity.hh
 load_entity.hh			
 surface_io_handler.hh
diff --git a/modules/io/src/mol/entity_io_sdf_handler.cc b/modules/io/src/mol/entity_io_sdf_handler.cc
index f9c49479f..72ec53cbc 100644
--- a/modules/io/src/mol/entity_io_sdf_handler.cc
+++ b/modules/io/src/mol/entity_io_sdf_handler.cc
@@ -16,392 +16,50 @@
 // along with this library; if not, write to the Free Software Foundation, Inc.,
 // 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 //------------------------------------------------------------------------------
-#include <iostream>
-#include <sstream>
-#include <iomanip>
+/*
+  Author: Tobias Schmidt
+ */
 
-#include <boost/filesystem/fstream.hpp>
-#include <boost/filesystem/convenience.hpp>
-#include <boost/algorithm/string.hpp>
-#include <boost/lexical_cast.hpp>
-#include <boost/format.hpp>
 #include <ost/log.hh>
-#include <ost/conop/conop.hh>
-#include <ost/io/io_exception.hh>
+#include <ost/io/sdf_writer.hh>
+#include <ost/io/sdf_reader.hh>
+
 #include "entity_io_sdf_handler.hh"
 
 namespace ost { namespace io {
 
-using boost::format;
 
 bool EntityIOSDFHandler::RequiresBuilder() const
 {
   return false;
 }
 
-// import data from provided stream
-void EntityIOSDFHandler::Import(mol::EntityHandle& ent, std::istream& instream)
+void EntityIOSDFHandler::Import(mol::EntityHandle& ent,
+                                std::istream& instream)
 {
-  String line;
-  mol::XCSEditor editor=ent.RequestXCSEditor(mol::BUFFERED_EDIT);
-  while (std::getline(instream,line)) {
-    ++line_num;
-
-    if (line_num<=4) {
-      ParseAndAddHeader(line, line_num, ent, editor);
-    } else if (line_num<=atom_count_+4) {
-      ParseAndAddAtom(line, line_num, ent, true, editor);
-    } else if (line_num<=bond_count_+atom_count_+4) {
-      ParseAndAddBond(line, line_num, ent, editor);
-    } else if (boost::iequals(line.substr(0,2), "> ")) {
-      // parse data items
-      int data_header_start = line.find('<');
-      String data_header = line.substr(data_header_start+1,line.rfind('>')-data_header_start-1);
-      if(data_header.empty()) {
-        String msg="Bad data line %d: Could not find data header";
-        throw IOException(str(format(msg) % line_num));
-      }
-      String data_value="";
-      while(std::getline(instream,line) && !boost::iequals(line, "")) {
-        data_value.append(line);
-      }
-      curr_chain_.SetStringProp(data_header, data_value);
-    } else if (boost::iequals(line, "$$$$")) {
-      LOGN_MESSAGE("MOLECULE " << curr_chain_.GetName() << " (" << chain_count_ << ") added.")
-      NextMolecule();
-    }
-  }
-
-  LOGN_MESSAGE("imported " << chain_count_ << " chains, " << residue_count_ 
-               << " residues, " << atom_count_ << " atoms");
+  SDFReader reader(instream);
+  reader.Import(ent);
 }
 
-
-
 void EntityIOSDFHandler::Import(mol::EntityHandle& ent,
                                 const boost::filesystem::path& loc)
 {
-  ClearState();
-  boost::filesystem::ifstream instream(loc);
-  if(!instream) throw IOException("could not open "+loc.string());  
-  this->Import(ent, instream);
-}
-
-void EntityIOSDFHandler::ClearState()
-{
-  curr_chain_=mol::ChainHandle();
-  curr_residue_=mol::ResidueHandle();
-  chain_count_=0;
-  residue_count_=0;
-  atom_count_=0;
-  bond_count_=0;
-  line_num=0;
-}
-
-void EntityIOSDFHandler::NextMolecule()
-{
-  residue_count_=0;
-  atom_count_=0;
-  bond_count_=0;
-  line_num=0;
-}
-
-void EntityIOSDFHandler::ParseAndAddHeader(const String& line, int line_num,
-                                           mol::EntityHandle& ent, mol::XCSEditor& editor)
-{
-  LOGN_TRACE( "line: [" << line << "]" );
-  format chain_fmter("%05i_%s");
-  switch(line_num)
-  {
-    case 1:  // title line
-    {
-      ++chain_count_;
-      String s_title=line;
-      String s_chain;
-      chain_fmter % chain_count_ % boost::trim_copy(s_title);
-      s_chain=chain_fmter.str();
-      if(s_chain.empty()) {
-        String msg="Bad molecule name line %d: Line is empty";
-        throw IOException(str(format(msg) % line_num));
-      }
-      curr_chain_=editor.InsertChain(s_chain);
-      LOGN_DUMP("new chain " << s_chain);
-
-      mol::ResidueKey rkey=boost::trim_copy(s_title);
-      mol::ResNum rnum(++residue_count_);
-      curr_residue_=editor.AppendResidue(curr_chain_, rkey, rnum);
-      LOGN_DUMP("new residue " << rkey << "(" << rnum << ")");
-      break;
-    }
-    case 2:  // user information line
-      break;
-    case 3:  // comments line
-      break;
-    case 4:  // counts line
-    {
-      String s_anum=line.substr(0,3);
-      try {
-        atom_count_=boost::lexical_cast<int>(boost::trim_copy(s_anum));
-      } catch(boost::bad_lexical_cast&) {
-        String msg="Bad counts line %d: Can't convert number of atoms"
-                   " '%s' to integral constant.";
-        throw IOException(str(format(msg) % line_num % s_anum));
-      }
-      String s_bnum=line.substr(3,3);
-      try {
-        bond_count_=boost::lexical_cast<int>(boost::trim_copy(s_bnum));
-      } catch(boost::bad_lexical_cast&) {
-        String msg="Bad counts line %d: Can't convert number of bonds"
-                   " '%s' to integral constant.";
-        throw IOException(str(format(msg) % line_num % s_bnum));
-      }
-      break;
-    }
-  }
-}
-
-void EntityIOSDFHandler::ParseAndAddAtom(const String& line, int line_num,
-                                         mol::EntityHandle& ent, bool hetatm,
-                                         mol::XCSEditor& editor)
-{
-
-  LOGN_TRACE( "line: [" << line << "]" );
-
-  if(line.length()<48 || line.length()>69) {
-    String msg="Bad atom line %d: Not correct number of characters on the"
-               " line: %i (should be between 48 and 69)";
-    throw IOException(str(format(msg) % line_num % line.length()));
-  }
-  int anum = line_num-4;  // start at 1 on fifth line since first four lines are header
-  String s_posx=line.substr(0,10);
-  String s_posy=line.substr(10,10);
-  String s_posz=line.substr(20,10);
-  String s_ele=line.substr(31,3);
-  String s_charge=line.substr(36,3);
-
-  geom::Vec3 apos;
-  try {
-    apos=geom::Vec3(boost::lexical_cast<Real>(boost::trim_copy(s_posx)),
-                    boost::lexical_cast<Real>(boost::trim_copy(s_posy)),
-                    boost::lexical_cast<Real>(boost::trim_copy(s_posz)));
-  } catch(boost::bad_lexical_cast&) {
-    String msg="Bad atom line %d: Can't convert coordinates to "
-               "floating point numbers";
-    throw IOException(str(format(msg) % line_num));
-  }
-
-  String ele=boost::trim_copy(s_ele);
-  String aname=boost::lexical_cast<String>(anum);
-
-  mol::AtomProp aprop;
-  aprop.element=ele;
-  aprop.radius=conop::Conopology::Instance().GetDefaultAtomRadius(ele);
-  aprop.mass=conop::Conopology::Instance().GetDefaultAtomMass(ele);
-  aprop.is_hetatm=hetatm;
-  
-  try {
-    aprop.charge=boost::lexical_cast<Real>(boost::trim_copy(s_charge));
-    if(aprop.charge != 0) {
-      aprop.charge=4-aprop.charge;
-    } //4-sdf_charge=real_charge if not 0
-  } catch(boost::bad_lexical_cast&) {
-    String msg="Bad atom line %d: Can't convert charge"
-               " '%s' to integral constant.";
-    throw IOException(str(format(msg) % line_num % s_charge));
-  }
-
-  LOGN_DUMP("adding atom " << aname << " (" << s_ele << ") @" << apos);
-
-  editor.InsertAtom(curr_residue_, aname,apos,aprop);
-}
-
-
-void EntityIOSDFHandler::ParseAndAddBond(const String& line, int line_num,
-                                         mol::EntityHandle& ent, mol::XCSEditor& editor)
-{
-
-  LOGN_TRACE( "line: [" << line << "]" );
-
-  if(line.length()<18 || line.length()>21) {
-    String msg="Bad bond line %d: Not correct number of characters on the"
-               " line: %i (should be between 18 and 21)";
-    throw IOException(str(format(msg) % line_num % line.length()));
-  }
-
-  String s_first_name=line.substr(0,3);
-  String s_second_name=line.substr(3,3);
-  String s_type=line.substr(6,3);
-  String first_name, second_name;
-  unsigned char type;
-  mol::BondHandle bond;
-  
-  first_name=boost::trim_copy(s_first_name);
-  second_name=boost::trim_copy(s_second_name);
-
-  try {
-    type=boost::lexical_cast<int>(boost::trim_copy(s_type));
-  } catch(boost::bad_lexical_cast&) {
-    String msg="Bad bond line %d: Can't convert bond type number"
-                " '%s' to integral constant.";
-    throw IOException(str(format(msg) % line_num % s_type));
-  }
-
-  mol::AtomHandle first,second;
-
-  first = ent.FindAtom(curr_chain_.GetName(), mol::ResNum(residue_count_), first_name);
-  second = ent.FindAtom(curr_chain_.GetName(), mol::ResNum(residue_count_), second_name);
-
-  if(first.IsValid() && second.IsValid()) {
-    bond = editor.Connect(first, second);
-    bond.SetBondOrder(type);
-  } else {
-    String msg="Bad bond line %d: Can't find the atom names '%s', '%s'"
-               " in entity.";
-    throw IOException(str(format(msg) % line_num % first % second));
-  }
-
-  LOGN_DUMP("adding bond " << s_first_name << " " << s_second_name << " (" << s_type << ") ");
-}
-
-namespace {
-
-  using boost::format;
-  
-  class SDFAtomWriter : public mol::EntityVisitor {
-    public:
-      SDFAtomWriter(std::ostream& ostream, std::map<long, int>& atom_indices) 
-      : ostr_(ostream), atom_indices_(atom_indices), counter_(0) {
-	atom_indices_.clear();
-      }
-    private:
-    public:
-      virtual bool VisitAtom(const mol::AtomHandle& atom) {
-        atom_indices_[atom.GetHashCode()] = ++counter_;
-        ostr_ << format("%10.4f") % atom.GetPos()[0]
-              << format("%10.4f") % atom.GetPos()[1]
-              << format("%10.4f ") % atom.GetPos()[2]
-              << format("%-3s") % atom.GetElement()
-              << " 0  0  0  0  0  0"
-              << std::endl;
-        return true;
-      }  
-    private:
-      std::ostream&      ostr_;
-      std::map<long, int>& atom_indices_;
-      int counter_;
-  };
-  
-  class SDFBondWriter : public mol::EntityVisitor {
-  public:
-    SDFBondWriter(std::ostream& ostream, std::map<long, int>& atom_indices) 
-      : ostr_(ostream), atom_indices_(atom_indices), counter_(0) {
-    }
-  private:
-  public:
-    virtual bool VisitAtom(const mol::AtomHandle& atom) {
-      counter_++;
-      mol::AtomHandleList atoms = atom.GetBondPartners();
-      mol::AtomHandleList::iterator atom_iter = atoms.begin();
-      for(; atom_iter != atoms.end(); ++atom_iter) {
-        int atom_index = atom_indices_.find((*atom_iter).GetHashCode())->second;
-        if(atom_index > counter_) {
-          int type = 1;
-          mol::BondHandle bond = atom.FindBondToAtom(*atom_iter);
-          if(bond.IsValid()) type = bond.GetBondOrder();
-          ostr_ << format("%3i") % counter_
-                << format("%3i") % atom_index
-                << format("%3i") % type
-                << "  0  0  0"
-                << std::endl;
-        }
-      }
-      return true;
-    }
-     
-  private:
-    std::ostream&      ostr_;
-    std::map<long, int>& atom_indices_;
-    int counter_;
-  };
-
-  class SDFWriter : public mol::EntityVisitor {
-  public:
-    SDFWriter(std::ostream& ostream)
-      : ostr_(ostream), counter_(0) {
-    }
-  private:
-  public:
-    virtual bool VisitChain(const mol::ChainHandle& chain) {
-      // print end of molecule line
-      if(counter_ != 0) {
-        ostr_ << "$$$$" << std::endl;
-        counter_ = 0;
-        atom_indices_.clear();
-      }
-      // print header lines
-      ostr_ << chain.GetName() << std::endl;
-      ostr_ << std::endl;
-      ostr_ << std::endl;
-
-      // print counts line
-      ostr_ << format("%3d") % chain.GetAtomCount()
-            << format("%3d") % chain.GetBondCount()
-            << "  0  0  0  0  0  0  0  0999 V2000"
-            << std::endl;
-      
-      // write atom block
-      SDFAtomWriter atom_writer(ostr_, atom_indices_);
-      mol::ChainHandle non_const_chain = chain;
-      non_const_chain.Apply(atom_writer);
-      
-      // write bond block
-      SDFBondWriter bond_writer(ostr_, atom_indices_);
-      non_const_chain.Apply(bond_writer);
-      
-      // write property block
-      //TODO: wirte property block
-      ostr_ << "M  END" << std::endl;
-      
-      // write data block
-      std::map<String,GenericPropValue> prop_map = non_const_chain.GetPropMap();
-      std::map<String,GenericPropValue>::iterator iter;
-      for(iter = prop_map.begin(); iter != prop_map.end(); ++iter) {
-        ostr_ << "> <" << (*iter).first << ">" << std::endl;
-        ostr_ << (*iter).second << std::endl;
-        ostr_ << std::endl;
-      }
-      
-      // write molecule endline
-      ostr_ << "$$$$" << std::endl;
-      
-      return true;
-    }
-
-
-    
-  private:
-    std::ostream&      ostr_;
-    int                counter_;
-    std::map<long,int> atom_indices_;
-  };
-
+  SDFReader reader(loc);
+  reader.Import(ent);
 }
 
 void EntityIOSDFHandler::Export(const mol::EntityView& ent,
                                 const boost::filesystem::path& loc) const {
 
-  boost::filesystem::ofstream outfile(loc);
-  if(!outfile) throw IOException("could not open "+loc.string()+" for writing");
-  this->Export(ent, outfile);
+  SDFWriter writer(loc);
+  writer.Write(ent);
 }
 
-// export data from entity view to provided stream
 void EntityIOSDFHandler::Export(const mol::EntityView& ent, 
                     std::ostream& outstream) const
 {
-  SDFWriter writer(outstream);  
-  mol::EntityView non_const_view = ent;
-  non_const_view.Apply(writer);
+  SDFWriter writer(outstream);
+  writer.Write(ent);
 }
 
 namespace {
@@ -436,16 +94,12 @@ bool EntityIOSDFHandler::ProvidesExport(const boost::filesystem::path& loc,
   return sdf_handler_is_responsible_for(loc, type);
 }
 
-
-
 mol::EntityHandle LoadSDF(const String& file_name) {
 
     mol::EntityHandle ent_handle=mol::CreateEntity();
     EntityIOSDFHandler ent_io;
     ent_io.Import(ent_handle,file_name);
 
-    LOG_DUMP("running conopology" << std::endl);
-
     return ent_handle;
 }
 
diff --git a/modules/io/src/mol/entity_io_sdf_handler.hh b/modules/io/src/mol/entity_io_sdf_handler.hh
index 1baa629d4..cebf2fa5e 100644
--- a/modules/io/src/mol/entity_io_sdf_handler.hh
+++ b/modules/io/src/mol/entity_io_sdf_handler.hh
@@ -20,7 +20,6 @@
 #define OST_IO_ENTITY_IO_PLUGIN_SDF_H
 
 #include <ost/io/mol/entity_io_handler.hh>
-#include <ost//mol/xcs_editor.hh>
 
 namespace ost { namespace io {
 
@@ -45,29 +44,6 @@ public:
 
   static String GetFormatName() { return String("Sdf"); }
   static String GetFormatDescription() { return String("Structure-data format from Molecular Design Limited"); }
-
-private:
-  void ClearState();
-
-  void NextMolecule();
-
-  void ParseAndAddHeader(const String& line, int line_num, mol::EntityHandle& ent,
-                         mol::XCSEditor& editor);
-
-  void ParseAndAddAtom(const String& line, int line_num, mol::EntityHandle& ent, 
-                       bool hetatm, mol::XCSEditor& editor);
-
-  void ParseAndAddBond(const String& line, int line_num, mol::EntityHandle& ent,
-                       mol::XCSEditor& editor);
-
-  mol::ChainHandle curr_chain_;
-  mol::ResidueHandle curr_residue_;
-  int chain_count_;
-  int residue_count_;
-  int atom_count_;
-  int bond_count_;
-  int line_num;
-
 };
 
 typedef EntityIOHandlerFactory<EntityIOSDFHandler> EntityIOSDFHandlerFactory;
diff --git a/modules/io/src/mol/sdf_reader.cc b/modules/io/src/mol/sdf_reader.cc
new file mode 100644
index 000000000..e2f0fabd2
--- /dev/null
+++ b/modules/io/src/mol/sdf_reader.cc
@@ -0,0 +1,275 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+/*
+  Author: Tobias Schmidt
+ */
+
+#include <boost/algorithm/string.hpp>
+#include <boost/format.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <ost/conop/conop.hh>
+#include <ost/io/io_exception.hh>
+#include <ost/log.hh>
+#include <ost/mol/xcs_editor.hh>
+
+#include "sdf_reader.hh"
+
+namespace ost { namespace io {
+
+using boost::format;
+
+SDFReader::SDFReader(const String& filename)
+  : infile_(filename), instream_(infile_) {
+  this->ClearState();
+}
+
+SDFReader::SDFReader(const boost::filesystem::path& loc)
+  : infile_(loc), instream_(infile_) {
+  this->ClearState();
+}
+
+SDFReader::SDFReader(std::istream& instream)
+  : infile_(), instream_(instream) {
+  this->ClearState();
+}
+
+// import data from provided stream
+void SDFReader::Import(mol::EntityHandle& ent)
+{
+  String line;
+  mol::XCSEditor editor=ent.RequestXCSEditor(mol::BUFFERED_EDIT);
+  while (std::getline(instream_,line)) {
+    ++line_num;
+
+    if (line_num<=4) {
+      ParseAndAddHeader(line, line_num, ent, editor);
+    } else if (line_num<=atom_count_+4) {
+      ParseAndAddAtom(line, line_num, ent, true, editor);
+    } else if (line_num<=bond_count_+atom_count_+4) {
+      ParseAndAddBond(line, line_num, ent, editor);
+    } else if (boost::iequals(line.substr(0,2), "> ")) {
+      // parse data items
+      int data_header_start = line.find('<');
+      if(data_header_start==-1) {
+        String msg="Bad data line %d: Could not find start or end of header";
+        throw IOException(str(format(msg) % line_num));
+      }
+      String data_header = line.substr(data_header_start+1,line.rfind('>')-data_header_start-1);
+      if(data_header.empty()) {
+        String msg="Bad data line %d: Could not find data header";
+        throw IOException(str(format(msg) % line_num));
+      }
+      String data_value="";
+      while(std::getline(instream_,line) && !boost::iequals(line, "")) {
+        data_value.append(line);
+      }
+      curr_chain_.SetStringProp(data_header, data_value);
+    } else if (boost::iequals(line, "$$$$")) {
+      LOGN_MESSAGE("MOLECULE " << curr_chain_.GetName() << " (" << chain_count_ << ") added.")
+      NextMolecule();
+    }
+  }
+
+  LOGN_MESSAGE("imported " << chain_count_ << " chains, " << residue_count_
+               << " residues, " << atom_count_ << " atoms");
+}
+
+void SDFReader::ClearState()
+{
+  curr_chain_=mol::ChainHandle();
+  curr_residue_=mol::ResidueHandle();
+  chain_count_=0;
+  residue_count_=0;
+  atom_count_=0;
+  bond_count_=0;
+  line_num=0;
+}
+
+void SDFReader::NextMolecule()
+{
+  residue_count_=0;
+  atom_count_=0;
+  bond_count_=0;
+  line_num=0;
+}
+
+void SDFReader::ParseAndAddHeader(const String& line, int line_num,
+                                  mol::EntityHandle& ent, mol::XCSEditor& editor)
+{
+  LOGN_TRACE( "line: [" << line << "]" );
+  format chain_fmter("%05i_%s");
+  switch(line_num)
+  {
+    case 1:  // title line
+    {
+      ++chain_count_;
+      String s_title=line;
+      String s_chain;
+      chain_fmter % chain_count_ % boost::trim_copy(s_title);
+      s_chain=chain_fmter.str();
+      if(s_chain.empty()) {
+        String msg="Bad molecule name line %d: Line is empty";
+        throw IOException(str(format(msg) % line_num));
+      }
+      curr_chain_=editor.InsertChain(s_chain);
+      LOGN_DUMP("new chain " << s_chain);
+
+      mol::ResidueKey rkey=boost::trim_copy(s_title);
+      mol::ResNum rnum(++residue_count_);
+      curr_residue_=editor.AppendResidue(curr_chain_, rkey, rnum);
+      LOGN_DUMP("new residue " << rkey << "(" << rnum << ")");
+      break;
+    }
+    case 2:  // user information line
+      break;
+    case 3:  // comments line
+      break;
+    case 4:  // counts line
+    {
+      String s_anum=line.substr(0,3);
+      try {
+        atom_count_=boost::lexical_cast<int>(boost::trim_copy(s_anum));
+      } catch(boost::bad_lexical_cast&) {
+        String msg="Bad counts line %d: Can't convert number of atoms"
+                   " '%s' to integral constant.";
+        throw IOException(str(format(msg) % line_num % s_anum));
+      }
+      String s_bnum=line.substr(3,3);
+      try {
+        bond_count_=boost::lexical_cast<int>(boost::trim_copy(s_bnum));
+      } catch(boost::bad_lexical_cast&) {
+        String msg="Bad counts line %d: Can't convert number of bonds"
+                   " '%s' to integral constant.";
+        throw IOException(str(format(msg) % line_num % s_bnum));
+      }
+      break;
+    }
+  }
+}
+
+void SDFReader::ParseAndAddAtom(const String& line, int line_num,
+                                mol::EntityHandle& ent, bool hetatm,
+                                mol::XCSEditor& editor)
+{
+
+  LOGN_TRACE( "line: [" << line << "]" );
+
+  if(line.length()<48 || line.length()>69) {
+    String msg="Bad atom line %d: Not correct number of characters on the"
+               " line: %i (should be between 48 and 69)";
+    throw IOException(str(format(msg) % line_num % line.length()));
+  }
+  int anum = line_num-4;  // start at 1 on fifth line since first four lines are header
+  String s_posx=line.substr(0,10);
+  String s_posy=line.substr(10,10);
+  String s_posz=line.substr(20,10);
+  String s_ele=line.substr(31,3);
+  String s_charge=line.substr(36,3);
+
+  geom::Vec3 apos;
+  try {
+    apos=geom::Vec3(boost::lexical_cast<Real>(boost::trim_copy(s_posx)),
+                    boost::lexical_cast<Real>(boost::trim_copy(s_posy)),
+                    boost::lexical_cast<Real>(boost::trim_copy(s_posz)));
+  } catch(boost::bad_lexical_cast&) {
+    String msg="Bad atom line %d: Can't convert coordinates to "
+               "floating point numbers";
+    throw IOException(str(format(msg) % line_num));
+  }
+
+  String ele=boost::trim_copy(s_ele);
+  String aname=boost::lexical_cast<String>(anum);
+
+  mol::AtomProp aprop;
+  aprop.element=ele;
+  aprop.radius=conop::Conopology::Instance().GetDefaultAtomRadius(ele);
+  aprop.mass=conop::Conopology::Instance().GetDefaultAtomMass(ele);
+  aprop.is_hetatm=hetatm;
+
+  try {
+    aprop.charge=boost::lexical_cast<Real>(boost::trim_copy(s_charge));
+    if(aprop.charge != 0) {
+      aprop.charge=4-aprop.charge;
+    } //4-sdf_charge=real_charge if not 0
+  } catch(boost::bad_lexical_cast&) {
+    String msg="Bad atom line %d: Can't convert charge"
+               " '%s' to integral constant.";
+    throw IOException(str(format(msg) % line_num % s_charge));
+  }
+
+  LOGN_DUMP("adding atom " << aname << " (" << s_ele << ") @" << apos);
+
+  editor.InsertAtom(curr_residue_, aname,apos,aprop);
+}
+
+
+void SDFReader::ParseAndAddBond(const String& line, int line_num,
+                                mol::EntityHandle& ent, mol::XCSEditor& editor)
+{
+
+  LOGN_TRACE( "line: [" << line << "]" );
+
+  if(line.length()<18 || line.length()>21) {
+    String msg="Bad bond line %d: Not correct number of characters on the"
+               " line: %i (should be between 18 and 21)";
+    throw IOException(str(format(msg) % line_num % line.length()));
+  }
+
+  String s_first_name=line.substr(0,3);
+  String s_second_name=line.substr(3,3);
+  String s_type=line.substr(6,3);
+  String first_name, second_name;
+  unsigned char type;
+  mol::BondHandle bond;
+
+  first_name=boost::trim_copy(s_first_name);
+  second_name=boost::trim_copy(s_second_name);
+
+  try {
+    type=boost::lexical_cast<int>(boost::trim_copy(s_type));
+    if (type<1 || type>8) {
+      String msg="Bad bond line %d: Bond type number"
+                      " '%s' not within accepted range (1-8).";
+      throw IOException(str(format(msg) % line_num % s_type));
+    }
+  } catch(boost::bad_lexical_cast&) {
+    String msg="Bad bond line %d: Can't convert bond type number"
+                " '%s' to integral constant.";
+    throw IOException(str(format(msg) % line_num % s_type));
+  }
+
+  mol::AtomHandle first,second;
+
+  first = ent.FindAtom(curr_chain_.GetName(), mol::ResNum(residue_count_), first_name);
+  second = ent.FindAtom(curr_chain_.GetName(), mol::ResNum(residue_count_), second_name);
+
+  if(first.IsValid() && second.IsValid()) {
+    bond = editor.Connect(first, second);
+    bond.SetBondOrder(type);
+  } else {
+    String msg="Bad bond line %d: Can't find the atom names '%s', '%s'"
+               " in entity.";
+    throw IOException(str(format(msg) % line_num % first % second));
+  }
+
+  LOGN_DUMP("adding bond " << s_first_name << " " << s_second_name << " (" << s_type << ") ");
+}
+
+}}
diff --git a/modules/io/src/mol/sdf_reader.hh b/modules/io/src/mol/sdf_reader.hh
new file mode 100644
index 000000000..0401e6ab7
--- /dev/null
+++ b/modules/io/src/mol/sdf_reader.hh
@@ -0,0 +1,67 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+/*
+  Author: Tobias Schmidt
+ */
+#ifndef OST_IO_SDF_READER_HH
+#define OST_IO_SDF_READER_HH
+
+#include <boost/filesystem/fstream.hpp>
+#include <ost/mol/mol.hh>
+#include <ost/io/module_config.hh>
+
+namespace ost { namespace io {
+
+class DLLEXPORT_OST_IO SDFReader {
+public:
+  SDFReader(const String& filename);
+  SDFReader(const boost::filesystem::path& loc);
+  SDFReader(std::istream& instream);
+
+  bool HasNext();
+
+  void Import(mol::EntityHandle& ent);
+
+private:
+  void ClearState();
+  void NextMolecule();
+
+  void ParseAndAddHeader(const String& line, int line_num, mol::EntityHandle& ent,
+                         mol::XCSEditor& editor);
+
+  void ParseAndAddAtom(const String& line, int line_num, mol::EntityHandle& ent,
+                       bool hetatm, mol::XCSEditor& editor);
+
+  void ParseAndAddBond(const String& line, int line_num, mol::EntityHandle& ent,
+                       mol::XCSEditor& editor);
+
+  mol::ChainHandle curr_chain_;
+  mol::ResidueHandle curr_residue_;
+  int chain_count_;
+  int residue_count_;
+  int atom_count_;
+  int bond_count_;
+  int line_num;
+  boost::filesystem::ifstream infile_;
+  std::istream& instream_;
+};
+
+}}
+
+#endif
diff --git a/modules/io/src/mol/sdf_writer.cc b/modules/io/src/mol/sdf_writer.cc
new file mode 100644
index 000000000..b6243b4a6
--- /dev/null
+++ b/modules/io/src/mol/sdf_writer.cc
@@ -0,0 +1,158 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+/*
+  Author: Tobias Schmidt
+ */
+
+#include "sdf_writer.hh"
+
+namespace ost { namespace io {
+
+using boost::format;
+
+namespace {
+
+  class SDFAtomWriter : public mol::EntityVisitor {
+    public:
+      SDFAtomWriter(std::ostream& ostream, std::map<long, int>& atom_indices)
+      : ostr_(ostream), atom_indices_(atom_indices), counter_(0) {
+  atom_indices_.clear();
+      }
+    private:
+    public:
+      virtual bool VisitAtom(const mol::AtomHandle& atom) {
+        atom_indices_[atom.GetHashCode()] = ++counter_;
+        ostr_ << format("%10.4f") % atom.GetPos()[0]
+              << format("%10.4f") % atom.GetPos()[1]
+              << format("%10.4f ") % atom.GetPos()[2]
+              << format("%-3s") % atom.GetElement()
+              << " 0  0  0  0  0  0"
+              << std::endl;
+        return true;
+      }
+    private:
+      std::ostream&      ostr_;
+      std::map<long, int>& atom_indices_;
+      int counter_;
+  };
+
+  class SDFBondWriter : public mol::EntityVisitor {
+  public:
+    SDFBondWriter(std::ostream& ostream, std::map<long, int>& atom_indices)
+      : ostr_(ostream), atom_indices_(atom_indices), counter_(0) {
+    }
+  private:
+  public:
+    virtual bool VisitAtom(const mol::AtomHandle& atom) {
+      counter_++;
+      mol::AtomHandleList atoms = atom.GetBondPartners();
+      mol::AtomHandleList::iterator atom_iter = atoms.begin();
+      for(; atom_iter != atoms.end(); ++atom_iter) {
+        int atom_index = atom_indices_.find((*atom_iter).GetHashCode())->second;
+        if(atom_index > counter_) {
+          int type = 1;
+          mol::BondHandle bond = atom.FindBondToAtom(*atom_iter);
+          if(bond.IsValid()) type = bond.GetBondOrder();
+          ostr_ << format("%3i") % counter_
+                << format("%3i") % atom_index
+                << format("%3i") % type
+                << "  0  0  0"
+                << std::endl;
+        }
+      }
+      return true;
+    }
+
+  private:
+    std::ostream&      ostr_;
+    std::map<long, int>& atom_indices_;
+    int counter_;
+  };
+}
+
+SDFWriter::SDFWriter(std::ostream& ostream)
+  : outfile_(), ostr_(ostream), counter_(0) {
+}
+
+SDFWriter::SDFWriter(const String& filename)
+  : outfile_(filename.c_str()), ostr_(outfile_), counter_(0) {
+}
+
+SDFWriter::SDFWriter(const boost::filesystem::path& filename)
+  : outfile_(filename.file_string().c_str()), ostr_(outfile_), counter_(0) {
+}
+
+void SDFWriter::Write(const mol::EntityView& ent) {
+  mol::EntityView non_const_view = ent;
+  non_const_view.Apply(*this);
+}
+
+void SDFWriter::Write(const mol::EntityHandle& ent) {
+  mol::EntityHandle non_const_handle = ent;
+  non_const_handle.Apply(*this);
+}
+
+bool SDFWriter::VisitChain(const mol::ChainHandle& chain) {
+  // print end of molecule line
+  if(counter_ != 0) {
+    ostr_ << "$$$$" << std::endl;
+    counter_ = 0;
+    atom_indices_.clear();
+  }
+
+  // print header lines
+  ostr_ << chain.GetName().substr(6) << std::endl;
+  ostr_ << std::endl;
+  ostr_ << std::endl;
+
+  // print counts line
+  ostr_ << format("%3d") % chain.GetAtomCount()
+        << format("%3d") % chain.GetBondCount()
+        << "  0  0  0  0            999 V2000"
+        << std::endl;
+
+  // write atom block
+  SDFAtomWriter atom_writer(ostr_, atom_indices_);
+  mol::ChainHandle non_const_chain = chain;
+  non_const_chain.Apply(atom_writer);
+
+  // write bond block
+  SDFBondWriter bond_writer(ostr_, atom_indices_);
+  non_const_chain.Apply(bond_writer);
+
+  // write property block
+  //TODO: write property block
+  ostr_ << "M  END" << std::endl;
+
+  // write data block
+  std::map<String,GenericPropValue> prop_map = non_const_chain.GetPropMap();
+  std::map<String,GenericPropValue>::iterator iter;
+  for(iter = prop_map.begin(); iter != prop_map.end(); ++iter) {
+    ostr_ << "> <" << (*iter).first << ">" << std::endl;
+    ostr_ << (*iter).second << std::endl;
+    ostr_ << std::endl;
+  }
+
+  // write molecule endline
+  ostr_ << "$$$$" << std::endl;
+
+  return true;
+}
+
+}}
diff --git a/modules/io/src/mol/sdf_writer.hh b/modules/io/src/mol/sdf_writer.hh
new file mode 100644
index 000000000..62a365d49
--- /dev/null
+++ b/modules/io/src/mol/sdf_writer.hh
@@ -0,0 +1,61 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2010 by the OpenStructure authors
+//
+// 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 3.0 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.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+/*
+  Author: Tobias Schmidt
+ */
+#ifndef OST_IO_SDF_WRITER_HH
+#define OST_IO_SDF_WRITER_HH
+
+#include <iostream>
+#include <sstream>
+#include <iomanip>
+
+#include <boost/filesystem/fstream.hpp>
+#include <boost/filesystem/convenience.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/format.hpp>
+#include <ost/log.hh>
+#include <ost/conop/conop.hh>
+#include <ost/io/io_exception.hh>
+#include <ost/mol/mol.hh>
+
+namespace ost { namespace io {
+
+class DLLEXPORT_OST_IO SDFWriter : public mol::EntityVisitor {
+public:
+  SDFWriter(std::ostream& ostream);
+  SDFWriter(const String& filename);
+  SDFWriter(const boost::filesystem::path& filename);
+
+  void Write(const mol::EntityView& ent);
+  void Write(const mol::EntityHandle& ent);
+
+private:
+  virtual bool VisitChain(const mol::ChainHandle& chain);
+
+  std::ofstream      outfile_;
+  std::ostream&      ostr_;
+  int                counter_;
+  std::map<long,int> atom_indices_;
+};
+
+}}
+
+#endif
diff --git a/modules/io/tests/test_io_pdb.cc b/modules/io/tests/test_io_pdb.cc
index 9be98cdba..54d7d49a9 100644
--- a/modules/io/tests/test_io_pdb.cc
+++ b/modules/io/tests/test_io_pdb.cc
@@ -16,6 +16,7 @@
 // along with this library; if not, write to the Free Software Foundation, Inc.,
 // 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 //------------------------------------------------------------------------------
+#include <ost/test_utils/compare_files.hh>
 #include <ost/mol/mol.hh>
 #include <ost/conop/conop.hh>
 #include <ost/io/mol/entity_io_pdb_handler.hh>
@@ -29,36 +30,6 @@ using boost::unit_test_framework::test_suite;
 using namespace ost;
 using namespace ost::io;
 
-
-bool compare_files(const String& test, const String& gold_standard)
-{
-  std::ifstream test_stream(test.c_str());
-  std::ifstream gold_stream(gold_standard.c_str());
-  String test_line, gold_line;
-  while (true) {
-    bool test_end=std::getline(test_stream, test_line);
-    bool gold_end=std::getline(gold_stream, gold_line);
-    if (!(test_end || gold_end)) {
-      return true;
-    }
-    if (!test_end) {
-      std::cerr << gold_standard << " contains additional line(s):"
-                << std::endl << gold_line;
-      return false;
-    }
-    if (!gold_end) {
-      std::cerr << test << " contains additional line(s):"
-                << std::endl << test_line;
-      return false;
-    }
-    if (gold_line!=test_line) {
-      std::cerr << "line mismatch:" << std::endl << "test: " << test_line 
-                << std::endl << "gold: " << gold_line;
-      return false;
-    }
-  }
-  return true;
-}
 BOOST_AUTO_TEST_SUITE( io )
 
 
diff --git a/modules/io/tests/test_io_sdf.cc b/modules/io/tests/test_io_sdf.cc
index 71c773f39..19cdf7f49 100644
--- a/modules/io/tests/test_io_sdf.cc
+++ b/modules/io/tests/test_io_sdf.cc
@@ -16,33 +16,110 @@
 // along with this library; if not, write to the Free Software Foundation, Inc.,
 // 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 //------------------------------------------------------------------------------
+#include <ost/test_utils/compare_files.hh>
 #include <ost/mol/mol.hh>
 #include <ost/io/mol/entity_io_sdf_handler.hh>
+#include <ost/io/save_entity.hh>
+#include <ost/io/io_exception.hh>
 #define BOOST_TEST_DYN_LINK
 #include <boost/test/unit_test.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string.hpp>
+using boost::unit_test_framework::test_suite;
 
 using namespace ost;
 using namespace ost::io;
 
 BOOST_AUTO_TEST_SUITE( io )
 
-
-BOOST_AUTO_TEST_CASE(test_io_sdf) 
+BOOST_AUTO_TEST_CASE(test_sdf_import_handler)
 {
-  const String fname("testfiles/test_in.sdf");
+  String fname("testfiles/sdf/compound.sdf");
 
   mol::EntityHandle eh=mol::CreateEntity();
   EntityIOSDFHandler sdfh;
 
-  // check format
   BOOST_CHECK(EntityIOSDFHandler::ProvidesImport("","sdf"));
   BOOST_CHECK(EntityIOSDFHandler::ProvidesImport(fname));
   BOOST_CHECK(EntityIOSDFHandler::ProvidesImport("test_in.SDF"));
 
+  BOOST_CHECK(EntityIOSDFHandler::ProvidesExport("","sdf"));
+  BOOST_CHECK(EntityIOSDFHandler::ProvidesExport(fname));
+  BOOST_CHECK(EntityIOSDFHandler::ProvidesExport("test_in.SDF"));
+
+  sdfh.Import(eh,"testfiles/sdf/compound.sdf");
+}
+
+BOOST_AUTO_TEST_CASE(simple_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+  sdfh.Import(eh,"testfiles/sdf/simple.sdf");
+
+  // check compounds/atoms/bonds count
+  BOOST_CHECK_EQUAL(eh.GetChainCount(), 1);
+  BOOST_CHECK_EQUAL(eh.GetAtomCount(),  6);
+  BOOST_CHECK_EQUAL(eh.GetBondCount(),  6);
+  BOOST_CHECK_CLOSE(eh.GetMass(), 121.545502, 1e-4);
+
+  // check atom/bond types
+  mol::AtomHandle ah=eh.GetAtomList()[0];
+  mol::AtomHandle ah2=eh.GetAtomList()[5];
+
+  BOOST_CHECK_EQUAL(ah.GetElement(),  "N");
+  BOOST_CHECK_EQUAL(ah2.GetElement(), "Cl");
+  BOOST_CHECK_CLOSE(ah.GetRadius(),  1.55, 1e-2);
+  BOOST_CHECK_CLOSE(ah2.GetRadius(), 1.75, 1e-2);
+  BOOST_CHECK_CLOSE(ah.GetMass(),  14.0067, 1e-4);
+  BOOST_CHECK_CLOSE(ah2.GetMass(), 35.453, 1e-3);
+  BOOST_CHECK_EQUAL(ah.GetBondCount(),  3);
+  BOOST_CHECK_EQUAL(ah2.GetBondCount(), 1);
+  BOOST_CHECK_EQUAL(ah.GetCharge(),  1);
+  BOOST_CHECK_EQUAL(ah2.GetCharge(), 0);
+
+  mol::BondHandle bh=ah.GetBondList()[0];
+  BOOST_CHECK_EQUAL(bh.GetBondOrder(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(multiple_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+  sdfh.Import(eh,"testfiles/sdf/multiple.sdf");
+
+  // check number of compounds
+  BOOST_CHECK_EQUAL(eh.GetChainCount(), 4);
+}
+
+BOOST_AUTO_TEST_CASE(properties_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+  sdfh.Import(eh,"testfiles/sdf/properties.sdf");
+
+  // check number of compounds
+  mol::ChainHandleList chl=eh.GetChainList();
+  int count=1;
+  for (mol::ChainHandleList::iterator i=chl.begin();i!=chl.end();++i,count++)
+  {
+    BOOST_REQUIRE(i->HasProp("prop_one"));
+    BOOST_REQUIRE(i->HasProp("prop_two"));
+    BOOST_CHECK_EQUAL(i->GetStringProp("prop_one"),
+                      boost::lexical_cast<std::string>(count));
+    BOOST_CHECK_EQUAL(i->GetStringProp("prop_two"),
+                      boost::lexical_cast<std::string>(count*(-2)));
+  }
+}
+
+BOOST_AUTO_TEST_CASE(read_sdf)
+{
+  const String fname("testfiles/sdf/compound.sdf");
+
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
   // check import
-  sdfh.Import(eh,"testfiles/test_in.sdf");
+  sdfh.Import(eh,"testfiles/sdf/compound.sdf");
 
   // check atoms/bonds
   BOOST_CHECK_EQUAL(eh.GetChainCount(), 4);
@@ -50,7 +127,7 @@ BOOST_AUTO_TEST_CASE(test_io_sdf)
   BOOST_CHECK_EQUAL(eh.GetBondCount(), 188);
 
   // check molecule name
-  mol::ChainHandle ch=eh.FindChain("00003_Displayed atoms");
+  mol::ChainHandle ch=eh.FindChain("00003_Test Ligand");
   BOOST_CHECK(ch.IsValid());
 
   // check properties
@@ -60,4 +137,97 @@ BOOST_AUTO_TEST_CASE(test_io_sdf)
                      0.543804f);
 }
 
+BOOST_AUTO_TEST_CASE(write_sdf)
+{
+  // this scope is required to force the writer stream to be closed before
+  // opening the file again in compare_files. Avoids a race condition.
+  {
+    mol::EntityHandle eh=mol::CreateEntity();
+    EntityIOSDFHandler sdfh;
+    sdfh.Import(eh,"testfiles/sdf/compound.sdf");
+    SaveEntity(eh, "testfiles/sdf/compound-out.sdf");
+  }
+  BOOST_CHECK(compare_files("testfiles/sdf/compound.sdf",
+                            "testfiles/sdf/compound-out.sdf"));
+}
+
+BOOST_AUTO_TEST_CASE(wrong_atomcount_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_atomcount.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_bondcount_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_bondcount.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_atomlinelength_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_atomlinelength.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_atompos_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_atompos.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_charge_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_charge.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_bondlinelength_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_bondlinelength.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_bondtype_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_bondtype.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_bondatomnumber_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_bondatomnumber.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(wrong_dataheader_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/wrong_dataheader.sdf"), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(empty_dataheader_error_sdf)
+{
+  mol::EntityHandle eh=mol::CreateEntity();
+  EntityIOSDFHandler sdfh;
+
+  BOOST_CHECK_THROW(sdfh.Import(eh,"testfiles/sdf/empty_dataheader.sdf"), IOException);
+}
+
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/io/tests/testfiles/test_in.sdf b/modules/io/tests/testfiles/sdf/compound.sdf
similarity index 97%
rename from modules/io/tests/testfiles/test_in.sdf
rename to modules/io/tests/testfiles/sdf/compound.sdf
index 9d7bdcfc1..66ddbb589 100644
--- a/modules/io/tests/testfiles/test_in.sdf
+++ b/modules/io/tests/testfiles/sdf/compound.sdf
@@ -1,7 +1,7 @@
-Displayed atoms
-                    3D
- Structure written by MMmdl.
- 45 47  0  0  1  0            999 V2000
+Test Ligand
+
+
+ 45 47  0  0  0  0            999 V2000
    35.9455    5.9021   22.1706 C   0  0  0  0  0  0
    34.6074    5.5226   22.4445 O   0  0  0  0  0  0
    33.9561    6.2475   23.4088 C   0  0  0  0  0  0
@@ -95,32 +95,29 @@ Displayed atoms
  26 27  1  0  0  0
  27 45  1  0  0  0
 M  END
+> <i_i_glide_confnum>
+2
+
 > <i_i_glide_lignum>
 1
 
-> <r_i_docking_score>
--8.84426
+> <i_i_glide_posenum>
+352
 
-> <r_i_glide_gscore>
+> <r_i_docking_score>
 -8.84426
 
-> <r_i_glide_lipo>
--2.84534
-
-> <r_i_glide_hbond>
--0.960751
-
-> <r_i_glide_metal>
--0
+> <r_i_glide_ecoul>
+-16.1644
 
-> <r_i_glide_rewards>
--0.799851
+> <r_i_glide_einternal>
+6.78671
 
-> <r_i_glide_evdw>
--46.0502
+> <r_i_glide_emodel>
+-96.9661
 
-> <r_i_glide_ecoul>
--16.1644
+> <r_i_glide_energy>
+-62.2146
 
 > <r_i_glide_erotb>
 0.517993
@@ -128,38 +125,41 @@ M  END
 > <r_i_glide_esite>
 -0.0291388
 
-> <r_i_glide_emodel>
--96.9661
+> <r_i_glide_evdw>
+-46.0502
 
-> <r_i_glide_energy>
--62.2146
+> <r_i_glide_gscore>
+-8.84426
 
-> <r_i_glide_einternal>
-6.78671
+> <r_i_glide_hbond>
+-0.960751
 
 > <r_i_glide_ligand_efficiency>
 -0.327565
 
+> <r_i_glide_ligand_efficiency_ln>
+-2.0588
+
 > <r_i_glide_ligand_efficiency_sa>
 -0.982695
 
-> <r_i_glide_ligand_efficiency_ln>
--2.0588
+> <r_i_glide_lipo>
+-2.84534
 
-> <i_i_glide_confnum>
-2
+> <r_i_glide_metal>
+-0
 
-> <i_i_glide_posenum>
-352
+> <r_i_glide_rewards>
+-0.799851
 
 > <r_i_glide_rmsd>
 0.6819
 
 $$$$
-Displayed atoms
-                    3D
- Structure written by MMmdl.
- 45 47  0  0  1  0            999 V2000
+Test Ligand
+
+
+ 45 47  0  0  0  0            999 V2000
    34.3938    4.9895   21.4537 C   0  0  0  0  0  0
    34.9786    5.7318   22.5298 O   0  0  0  0  0  0
    34.1450    6.3862   23.4047 C   0  0  0  0  0  0
@@ -253,32 +253,29 @@ Displayed atoms
  26 27  1  0  0  0
  27 45  1  0  0  0
 M  END
+> <i_i_glide_confnum>
+14
+
 > <i_i_glide_lignum>
 1
 
-> <r_i_docking_score>
--8.79327
+> <i_i_glide_posenum>
+302
 
-> <r_i_glide_gscore>
+> <r_i_docking_score>
 -8.79327
 
-> <r_i_glide_lipo>
--2.69167
-
-> <r_i_glide_hbond>
--0.966475
-
-> <r_i_glide_metal>
--0
+> <r_i_glide_ecoul>
+-16.9687
 
-> <r_i_glide_rewards>
--0.777126
+> <r_i_glide_einternal>
+5.76514
 
-> <r_i_glide_evdw>
--46.4187
+> <r_i_glide_emodel>
+-98.4298
 
-> <r_i_glide_ecoul>
--16.9687
+> <r_i_glide_energy>
+-63.3874
 
 > <r_i_glide_erotb>
 0.517993
@@ -286,38 +283,41 @@ M  END
 > <r_i_glide_esite>
 -0.00975737
 
-> <r_i_glide_emodel>
--98.4298
+> <r_i_glide_evdw>
+-46.4187
 
-> <r_i_glide_energy>
--63.3874
+> <r_i_glide_gscore>
+-8.79327
 
-> <r_i_glide_einternal>
-5.76514
+> <r_i_glide_hbond>
+-0.966475
 
 > <r_i_glide_ligand_efficiency>
 -0.325677
 
+> <r_i_glide_ligand_efficiency_ln>
+-2.04693
+
 > <r_i_glide_ligand_efficiency_sa>
 -0.97703
 
-> <r_i_glide_ligand_efficiency_ln>
--2.04693
+> <r_i_glide_lipo>
+-2.69167
 
-> <i_i_glide_confnum>
-14
+> <r_i_glide_metal>
+-0
 
-> <i_i_glide_posenum>
-302
+> <r_i_glide_rewards>
+-0.777126
 
 > <r_i_glide_rmsd>
 0.605551
 
 $$$$
-Displayed atoms
-                    3D
- Structure written by MMmdl.
- 45 47  0  0  1  0            999 V2000
+Test Ligand
+
+
+ 45 47  0  0  0  0            999 V2000
    36.2241    5.1749   22.8554 C   0  0  0  0  0  0
    35.0609    5.8871   22.4479 O   0  0  0  0  0  0
    34.2768    6.4601   23.4276 C   0  0  0  0  0  0
@@ -411,32 +411,29 @@ Displayed atoms
  26 27  1  0  0  0
  27 45  1  0  0  0
 M  END
+> <i_i_glide_confnum>
+1
+
 > <i_i_glide_lignum>
 1
 
-> <r_i_docking_score>
--8.70173
+> <i_i_glide_posenum>
+177
 
-> <r_i_glide_gscore>
+> <r_i_docking_score>
 -8.70173
 
-> <r_i_glide_lipo>
--2.74283
-
-> <r_i_glide_hbond>
--0.97397
-
-> <r_i_glide_metal>
--0
+> <r_i_glide_ecoul>
+-15.8862
 
-> <r_i_glide_rewards>
--0.764823
+> <r_i_glide_einternal>
+1.84397
 
-> <r_i_glide_evdw>
--46.5538
+> <r_i_glide_emodel>
+-99.0481
 
-> <r_i_glide_ecoul>
--15.8862
+> <r_i_glide_energy>
+-62.44
 
 > <r_i_glide_erotb>
 0.517993
@@ -444,38 +441,41 @@ M  END
 > <r_i_glide_esite>
 -0.0274759
 
-> <r_i_glide_emodel>
--99.0481
+> <r_i_glide_evdw>
+-46.5538
 
-> <r_i_glide_energy>
--62.44
+> <r_i_glide_gscore>
+-8.70173
 
-> <r_i_glide_einternal>
-1.84397
+> <r_i_glide_hbond>
+-0.97397
 
 > <r_i_glide_ligand_efficiency>
 -0.322286
 
+> <r_i_glide_ligand_efficiency_ln>
+-2.02562
+
 > <r_i_glide_ligand_efficiency_sa>
 -0.966858
 
-> <r_i_glide_ligand_efficiency_ln>
--2.02562
+> <r_i_glide_lipo>
+-2.74283
 
-> <i_i_glide_confnum>
-1
+> <r_i_glide_metal>
+-0
 
-> <i_i_glide_posenum>
-177
+> <r_i_glide_rewards>
+-0.764823
 
 > <r_i_glide_rmsd>
 0.543804
 
 $$$$
-Displayed atoms
-                    3D
- Structure written by MMmdl.
- 45 47  0  0  1  0            999 V2000
+Test Ligand
+
+
+ 45 47  0  0  0  0            999 V2000
    36.1312    5.0238   22.8745 C   0  0  0  0  0  0
    35.3492    6.1234   22.4285 O   0  0  0  0  0  0
    34.5144    6.6833   23.3611 C   0  0  0  0  0  0
@@ -569,32 +569,29 @@ Displayed atoms
  26 27  1  0  0  0
  27 45  1  0  0  0
 M  END
+> <i_i_glide_confnum>
+9
+
 > <i_i_glide_lignum>
 1
 
-> <r_i_docking_score>
--8.69162
+> <i_i_glide_posenum>
+294
 
-> <r_i_glide_gscore>
+> <r_i_docking_score>
 -8.69162
 
-> <r_i_glide_lipo>
--2.92829
-
-> <r_i_glide_hbond>
--0.870172
-
-> <r_i_glide_metal>
--0
+> <r_i_glide_ecoul>
+-14.7519
 
-> <r_i_glide_rewards>
--0.764823
+> <r_i_glide_einternal>
+5.89466
 
-> <r_i_glide_evdw>
--48.432
+> <r_i_glide_emodel>
+-97.7232
 
-> <r_i_glide_ecoul>
--14.7519
+> <r_i_glide_energy>
+-63.1839
 
 > <r_i_glide_erotb>
 0.517993
@@ -602,29 +599,32 @@ M  END
 > <r_i_glide_esite>
 -0.0119369
 
-> <r_i_glide_emodel>
--97.7232
+> <r_i_glide_evdw>
+-48.432
 
-> <r_i_glide_energy>
--63.1839
+> <r_i_glide_gscore>
+-8.69162
 
-> <r_i_glide_einternal>
-5.89466
+> <r_i_glide_hbond>
+-0.870172
 
 > <r_i_glide_ligand_efficiency>
 -0.321912
 
+> <r_i_glide_ligand_efficiency_ln>
+-2.02327
+
 > <r_i_glide_ligand_efficiency_sa>
 -0.965735
 
-> <r_i_glide_ligand_efficiency_ln>
--2.02327
+> <r_i_glide_lipo>
+-2.92829
 
-> <i_i_glide_confnum>
-9
+> <r_i_glide_metal>
+-0
 
-> <i_i_glide_posenum>
-294
+> <r_i_glide_rewards>
+-0.764823
 
 > <r_i_glide_rmsd>
 0.463026
diff --git a/modules/io/tests/testfiles/sdf/empty_dataheader.sdf b/modules/io/tests/testfiles/sdf/empty_dataheader.sdf
new file mode 100644
index 000000000..9de4a7b63
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/empty_dataheader.sdf
@@ -0,0 +1,48 @@
+Simple Ligand 1
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> <>
+1
+
+> <prop_two>
+-2
+
+$$$$
+Simple Ligand 2
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> <prop_one>
+2
+
+> <prop_two>
+-4
+
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/multiple.sdf b/modules/io/tests/testfiles/sdf/multiple.sdf
new file mode 100644
index 000000000..7570f2d4d
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/multiple.sdf
@@ -0,0 +1,72 @@
+Simple Ligand 1
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
+Simple Ligand 2
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
+Simple Ligand 3
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
+Simple Ligand 4
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/properties.sdf b/modules/io/tests/testfiles/sdf/properties.sdf
new file mode 100644
index 000000000..edaf39129
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/properties.sdf
@@ -0,0 +1,48 @@
+Simple Ligand 1
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> <prop_one>
+1
+
+> <prop_two>
+-2
+
+$$$$
+Simple Ligand 2
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> <prop_one>
+2
+
+> <prop_two>
+-4
+
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/simple.sdf b/modules/io/tests/testfiles/sdf/simple.sdf
new file mode 100644
index 000000000..beeb1acdd
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/simple.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_atomcount.sdf b/modules/io/tests/testfiles/sdf/wrong_atomcount.sdf
new file mode 100644
index 000000000..ff9049fac
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_atomcount.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  i  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_atomlinelength.sdf b/modules/io/tests/testfiles/sdf/wrong_atomlinelength.sdf
new file mode 100644
index 000000000..42f30f1a5
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_atomlinelength.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0 
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_atompos.sdf b/modules/io/tests/testfiles/sdf/wrong_atompos.sdf
new file mode 100644
index 000000000..50d5268a0
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_atompos.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+0.0000 1.0000    0.0000        O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_bondatomnumber.sdf b/modules/io/tests/testfiles/sdf/wrong_bondatomnumber.sdf
new file mode 100644
index 000000000..590827c4d
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_bondatomnumber.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  8  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_bondcount.sdf b/modules/io/tests/testfiles/sdf/wrong_bondcount.sdf
new file mode 100644
index 000000000..3d6194d1b
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_bondcount.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  i  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_bondlinelength.sdf b/modules/io/tests/testfiles/sdf/wrong_bondlinelength.sdf
new file mode 100644
index 000000000..5be46a393
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_bondlinelength.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_bondtype.sdf b/modules/io/tests/testfiles/sdf/wrong_bondtype.sdf
new file mode 100644
index 000000000..432c10464
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_bondtype.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6 -1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_charge.sdf b/modules/io/tests/testfiles/sdf/wrong_charge.sdf
new file mode 100644
index 000000000..01dba009e
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_charge.sdf
@@ -0,0 +1,18 @@
+Simple Ligand
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  i  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+$$$$
diff --git a/modules/io/tests/testfiles/sdf/wrong_dataheader.sdf b/modules/io/tests/testfiles/sdf/wrong_dataheader.sdf
new file mode 100644
index 000000000..b895448e6
--- /dev/null
+++ b/modules/io/tests/testfiles/sdf/wrong_dataheader.sdf
@@ -0,0 +1,48 @@
+Simple Ligand 1
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> prop_one>
+1
+
+> <prop_two>
+-2
+
+$$$$
+Simple Ligand 2
+
+ Teststructure
+  6  6  0  0  1  0            999 V2000
+    0.0000    0.0000    0.0000 N   0  3  0  0  0  0
+    1.0000    0.0000    0.0000 C   0  0  0  0  0  0
+    0.0000    1.0000    0.0000 O   0  0  0  0  0  0
+    1.0000    1.0000    0.0000 S   0  0  0  0  0  0
+    2.0000    2.0000    0.0000 C   0  0  0  0  0  0
+   -1.0000   -1.0000    0.0000 Cl  0  0  0  0  0  0
+  1  2  2  0  0  0
+  1  3  1  0  0  0
+  1  6  1  0  0  0
+  2  4  1  0  0  0
+  3  4  1  0  0  0
+  4  5  3  0  0  0
+M  END
+> <prop_one>
+2
+
+> <prop_two>
+-4
+
+$$$$
-- 
GitLab