Commit 8384d7e0 authored by Dick Hollenbeck's avatar Dick Hollenbeck

implement some of the DIR_LIB_SOURCE Read*() functions

parent 80f21358
...@@ -212,6 +212,7 @@ add_subdirectory(polygon) ...@@ -212,6 +212,7 @@ add_subdirectory(polygon)
add_subdirectory(polygon/kbool/src) add_subdirectory(polygon/kbool/src)
add_subdirectory(potrace) add_subdirectory(potrace)
add_subdirectory(bitmap2component) add_subdirectory(bitmap2component)
#add_subdirectory(new)
############# #############
# Resources # # Resources #
......
...@@ -56,10 +56,11 @@ else(DOXYGEN_FOUND) ...@@ -56,10 +56,11 @@ else(DOXYGEN_FOUND)
endif() endif()
include_directories( ${CMAKE_SOURCE_DIR} ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} )
add_executable( test_dir_lib_source sch_dir_lib_source.cpp ) add_executable( test_dir_lib_source sch_dir_lib_source.cpp ${PROJECT_SOURCE_DIR}/common/richio.cpp )
#add_executable( test_dir_lib_source EXCLUDE_FROM_ALL sch_dir_lib_source.cpp ) #add_executable( test_dir_lib_source EXCLUDE_FROM_ALL sch_dir_lib_source.cpp )
target_link_libraries( test_dir_lib_source ${wxWidgets_LIBRARIES} ) target_link_libraries( test_dir_lib_source ${wxWidgets_LIBRARIES} )
#target_link_libraries( test_dir_lib_source common ${wxWidgets_LIBRARIES} )
...@@ -352,21 +352,6 @@ Show architecture here. ...@@ -352,21 +352,6 @@ Show architecture here.
*/ */
typedef std::string STRING; typedef std::string STRING;
/**
* Type STRING_TOKS
* documents a container which holds a sequence of s-expressions suitable for parsing
* with DSNLEXER. This can either be a sequence of DSN_SYMBOLs or a sequence of
* fully parenthesis delimited s-expressions. There are 2 types: <ol>
* <li> R C R33 "quoted-name" J2
* <li> (part R ())(part C ())
* </ol>
* Notice that in the 1st example, there are 5 tokens in sequence, and in the
* 2nd example there are two top most s-expressions in sequence. So the counts
* in these are 5 and 2 respectively.
*/
typedef std::dequeue<STRING> STRING_TOKS;
typedef std::dequeue<STRING> STRINGS; typedef std::dequeue<STRING> STRINGS;
//typedef std::vector<wxString> WSTRINGS; //typedef std::vector<wxString> WSTRINGS;
......
...@@ -34,8 +34,13 @@ ...@@ -34,8 +34,13 @@
http://www.softagalleria.net/dirent.php http://www.softagalleria.net/dirent.php
wx has these but they are based on wxString which can be wchar_t based and wx should wx has these but they are based on wxString which can be wchar_t based and wx should
not be introduced at a level this low. not be introduced at a level this low.
Part files: have the general form partname.part[.revN...]
Categories: are any subdirectories immediately below the sourceURI, one level only.
Part names: [category/]partname[/revN...]
*/ */
#include <sch_dir_lib_source.h> #include <sch_dir_lib_source.h>
using namespace SCH; using namespace SCH;
...@@ -60,7 +65,7 @@ using namespace std; ...@@ -60,7 +65,7 @@ using namespace std;
/** /**
* Class DIR_WRAP * Class DIR_WRAP
* provides a destructor which may be invoked if an exception is thrown. * provides a destructor which is invoked if an exception is thrown.
*/ */
class DIR_WRAP class DIR_WRAP
{ {
...@@ -77,12 +82,13 @@ public: ...@@ -77,12 +82,13 @@ public:
DIR* operator->() { return dir; } DIR* operator->() { return dir; }
DIR* operator*() { return dir; } DIR* operator*() { return dir; }
operator bool () { return dir!=0; }
}; };
/** /**
* Class FILE_WRAP * Class FILE_WRAP
* provides a destructor which may be invoked if an exception is thrown. * provides a destructor which is invoked if an exception is thrown.
*/ */
class FILE_WRAP class FILE_WRAP
{ {
...@@ -119,24 +125,30 @@ static const char* strrstr( const char* haystack, const char* needle ) ...@@ -119,24 +125,30 @@ static const char* strrstr( const char* haystack, const char* needle )
return ret; return ret;
} }
/**
static const char* endsWithRev( const char* cp, const char* limit ) * Function endsWithRev
* returns a pointer to the final string segment: "revN..." or NULL if none.
* @param start is the beginning of string segment to test, the partname or
* any middle portion of it.
* @param tail is a pointer to the terminating nul.
* @param separator is the separating byte, expected: '.' or '/', depending on context.
*/
static const char* endsWithRev( const char* start, const char* tail, char separator )
{ {
// find last instance of ".rev" bool sawDigit = false;
cp = strrstr( cp, ".rev" );
if( cp )
{
const char* rev = cp + 1;
cp += sizeof( ".rev" )-1;
while( isdigit( *cp ) )
++cp;
if( cp != limit ) // there is garbage after "revN.." while( isdigit(*--tail) && tail>start )
rev = 0; {
sawDigit = true;
}
return rev; if( sawDigit && tail-3 >= start && tail[-3] == separator )
{
tail -= 2;
if( tail[0]=='r' && tail[1]=='e' && tail[2]=='v' )
{
return tail;
}
} }
return 0; return 0;
...@@ -168,7 +180,7 @@ bool DIR_LIB_SOURCE::makePartFileName( const char* aEntry, ...@@ -168,7 +180,7 @@ bool DIR_LIB_SOURCE::makePartFileName( const char* aEntry,
// if versioning, test for a trailing "revN.." type of string // if versioning, test for a trailing "revN.." type of string
if( useVersioning ) if( useVersioning )
{ {
const char* rev = endsWithRev( cp + sizeof(".part") - 1, limit ); const char* rev = endsWithRev( cp + sizeof(".part") - 1, limit, '.' );
if( rev ) if( rev )
{ {
if( aCategory.size() ) if( aCategory.size() )
...@@ -193,10 +205,51 @@ static bool isCategoryName( const char* aName ) ...@@ -193,10 +205,51 @@ static bool isCategoryName( const char* aName )
} }
#define MAX_PART_FILE_SIZE (1*1024*1024) // sanity check void DIR_LIB_SOURCE::readSExpression( STRING* aResult, const STRING& aFilename ) throw( IO_ERROR )
{
FILE_WRAP fw = open( aFilename.c_str(), O_RDONLY );
DIR_LIB_SOURCE::DIR_LIB_SOURCE( const STRING& aDirectoryPath, bool doUseVersioning ) if( fw == -1 )
throw( IO_ERROR ) {
STRING msg = aFilename;
msg += " cannot be open()ed for reading";
throw IO_ERROR( msg.c_str() );
}
struct stat fs;
fstat( fw, &fs );
// sanity check on file size
if( fs.st_size > (1*1024*1024) )
{
STRING msg = aFilename;
msg += " seems too big. ( > 1mbyte )";
throw IO_ERROR( msg.c_str() );
}
// we reuse the same readBuffer, which is not thread safe, but the API
// is not expected to be thread safe.
readBuffer.resize( fs.st_size );
size_t count = read( fw, &readBuffer[0], fs.st_size );
if( count != (size_t) fs.st_size )
{
STRING msg = aFilename;
msg += " cannot be read";
throw IO_ERROR( msg.c_str() );
}
// std::string chars are not gauranteed to be contiguous in
// future implementations of C++, so this is why we did not read into
// aResult directly.
aResult->assign( &readBuffer[0], count );
}
DIR_LIB_SOURCE::DIR_LIB_SOURCE( const STRING& aDirectoryPath,
bool doUseVersioning ) throw( IO_ERROR ) :
readBuffer( 512 )
{ {
useVersioning = doUseVersioning; useVersioning = doUseVersioning;
sourceURI = aDirectoryPath; sourceURI = aDirectoryPath;
...@@ -225,19 +278,121 @@ DIR_LIB_SOURCE::~DIR_LIB_SOURCE() ...@@ -225,19 +278,121 @@ DIR_LIB_SOURCE::~DIR_LIB_SOURCE()
} }
void DIR_LIB_SOURCE::GetCategoricalPartNames( STRINGS* aResults, const STRING& aCategory )
throw( IO_ERROR )
{
aResults->clear();
if( aCategory.size() )
{
STRING lower = aCategory + "/";
STRING upper = aCategory + char( '/' + 1 );
DIR_CACHE::const_iterator limit = sweets.upper_bound( upper );
for( DIR_CACHE::const_iterator it = sweets.lower_bound( lower ); it!=limit; ++it )
{
const char* start = it->first.c_str();
size_t len = it->first.size();
if( !endsWithRev( start, start+len, '/' ) )
aResults->push_back( it->first );
}
}
else
{
for( DIR_CACHE::const_iterator it = sweets.begin(); it!=sweets.end(); ++it )
{
const char* start = it->first.c_str();
size_t len = it->first.size();
if( !endsWithRev( start, start+len, '/' ) )
aResults->push_back( it->first );
}
}
}
void DIR_LIB_SOURCE::ReadPart( STRING* aResult, const STRING& aPartName, const STRING& aRev )
throw( IO_ERROR )
{
STRING partname = aPartName;
if( aRev.size() )
partname += "/" + aRev;
DIR_CACHE::iterator it = sweets.find( partname );
if( it == sweets.end() ) // part not found
{
partname += " not found.";
throw IO_ERROR( partname.c_str() );
}
if( !it->second ) // if the sweet string is not loaded yet
{
STRING filename = sourceURI + "/" + aPartName + ".part";
if( aRev.size() )
{
filename += "." + aRev;
}
it->second = new STRING();
readSExpression( it->second, filename );
}
*aResult = *it->second;
}
void DIR_LIB_SOURCE::ReadParts( STRINGS* aResults, const STRINGS& aPartNames )
throw( IO_ERROR )
{
aResults->clear();
for( STRINGS::const_iterator n = aPartNames.begin(); n!=aPartNames.end(); ++n )
{
aResults->push_back( STRING() );
ReadPart( &aResults->back(), *n );
}
}
void DIR_LIB_SOURCE::GetCategories( STRINGS* aResults ) throw( IO_ERROR )
{
*aResults = categories;
}
#if defined(DEBUG)
#include <richio.h>
void DIR_LIB_SOURCE::Show() void DIR_LIB_SOURCE::Show()
{ {
printf( "categories:\n" ); printf( "Show categories:\n" );
for( STRINGS::const_iterator it = categories.begin(); it!=categories.end(); ++it ) for( STRINGS::const_iterator it = categories.begin(); it!=categories.end(); ++it )
printf( " '%s'\n", it->c_str() ); printf( " '%s'\n", it->c_str() );
printf( "\n" ); printf( "\n" );
printf( "parts:\n" ); printf( "Show parts:\n" );
for( DIR_CACHE::const_iterator it = sweets.begin(); it != sweets.end(); ++it ) for( DIR_CACHE::const_iterator it = sweets.begin(); it != sweets.end(); ++it )
{ {
printf( " '%s'\n", it->first.c_str() ); printf( " '%s'\n", it->first.c_str() );
if( it->second )
{
STRING_LINE_READER slr( *it->second, wxString( wxConvertMB2WX( it->first.c_str() ) ) );
while( slr.ReadLine() )
{
printf( " %s", (char*) slr );
}
printf( "\n" );
}
} }
} }
#endif
void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
...@@ -249,7 +404,7 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) ...@@ -249,7 +404,7 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
DIR_WRAP dir = opendir( curDir.c_str() ); DIR_WRAP dir = opendir( curDir.c_str() );
if( !*dir ) if( !dir )
{ {
STRING msg = strerror( errno ); STRING msg = strerror( errno );
msg += "; scanning directory " + curDir; msg += "; scanning directory " + curDir;
...@@ -257,10 +412,8 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) ...@@ -257,10 +412,8 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
} }
struct stat fs; struct stat fs;
STRING partName; STRING partName;
STRING fileName; STRING fileName;
dirent* entry; dirent* entry;
while( (entry = readdir( *dir )) != NULL ) while( (entry = readdir( *dir )) != NULL )
...@@ -270,8 +423,6 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) ...@@ -270,8 +423,6 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
fileName = curDir + "/" + entry->d_name; fileName = curDir + "/" + entry->d_name;
//D( printf("name: '%s'\n", fileName.c_str() );)
if( !stat( fileName.c_str(), &fs ) ) if( !stat( fileName.c_str(), &fs ) )
{ {
if( S_ISREG( fs.st_mode ) && makePartFileName( entry->d_name, aCategory, &partName ) ) if( S_ISREG( fs.st_mode ) && makePartFileName( entry->d_name, aCategory, &partName ) )
...@@ -286,15 +437,16 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) ...@@ -286,15 +437,16 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
*/ */
sweets[partName] = NULL; // NULL for now, load the sweet later. sweets[partName] = NULL; // NULL for now, load the sweet later.
//D( printf("part: %s\n", partName.c_str() );)
} }
else if( S_ISDIR( fs.st_mode ) && !aCategory.size() && isCategoryName( entry->d_name ) ) else if( S_ISDIR( fs.st_mode ) && !aCategory.size() && isCategoryName( entry->d_name ) )
{ {
// only one level of recursion is used, controlled by the // only one level of recursion is used, controlled by the
// emptiness of aCategory. // emptiness of aCategory.
//D( printf("category: %s\n", entry->d_name );)
categories.push_back( entry->d_name ); categories.push_back( entry->d_name );
// somebody needs to test Windows (mingw), make sure it can
// handle opendir() recursively
doOneDir( entry->d_name ); doOneDir( entry->d_name );
} }
else else
...@@ -306,15 +458,53 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR ) ...@@ -306,15 +458,53 @@ void DIR_LIB_SOURCE::doOneDir( const STRING& aCategory ) throw( IO_ERROR )
} }
#if 1 || defined( TEST_DIR_LIB_SOURCE ) #if (1 || defined( TEST_DIR_LIB_SOURCE )) && defined(DEBUG)
int main( int argc, char** argv ) int main( int argc, char** argv )
{ {
STRINGS partnames;
STRINGS sweets;
try try
{ {
DIR_LIB_SOURCE uut( argv[1] ? argv[1] : "", true ); DIR_LIB_SOURCE uut( argv[1] ? argv[1] : "", true );
// initially, only the DIR_CACHE sweets and STRING categories are loaded:
uut.Show(); uut.Show();
uut.GetCategoricalPartNames( &partnames, "Category" );
printf( "GetCategoricalPartNames(Category):\n" );
for( STRINGS::const_iterator it = partnames.begin(); it!=partnames.end(); ++it )
{
printf( " '%s'\n", it->c_str() );
}
uut.ReadParts( &sweets, partnames );
// fetch the part names for ALL categories.
uut.GetCategoricalPartNames( &partnames );
printf( "GetCategoricalPartNames(ALL):\n" );
for( STRINGS::const_iterator it = partnames.begin(); it!=partnames.end(); ++it )
{
printf( " '%s'\n", it->c_str() );
}
uut.ReadParts( &sweets, partnames );
printf( "Sweets for ALL parts:\n" );
STRINGS::const_iterator pn = partnames.begin();
for( STRINGS::const_iterator it = sweets.begin(); it!=sweets.end(); ++it, ++pn )
{
printf( " %s: %s", pn->c_str(), it->c_str() );
}
}
catch( std::exception& ex )
{
printf( "std::exception\n" );
} }
catch( IO_ERROR ioe ) catch( IO_ERROR ioe )
...@@ -327,4 +517,3 @@ int main( int argc, char** argv ) ...@@ -327,4 +517,3 @@ int main( int argc, char** argv )
#endif #endif
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
#include <sch_lib.h> #include <sch_lib.h>
#include <map> #include <map>
#include <vector>
/** /**
...@@ -54,8 +55,11 @@ class DIR_LIB_SOURCE : public LIB_SOURCE ...@@ -54,8 +55,11 @@ class DIR_LIB_SOURCE : public LIB_SOURCE
bool useVersioning; ///< use files with extension ".revNNN..", else not bool useVersioning; ///< use files with extension ".revNNN..", else not
DIR_CACHE sweets; DIR_CACHE sweets; ///< @todo, don't really need to cache the sweets, only the partnames.
STRINGS categories; STRINGS categories;
std::vector<char> readBuffer; ///< used by readSExpression()
/** /**
* Function isPartFileName * Function isPartFileName
...@@ -75,6 +79,13 @@ class DIR_LIB_SOURCE : public LIB_SOURCE ...@@ -75,6 +79,13 @@ class DIR_LIB_SOURCE : public LIB_SOURCE
bool makePartFileName( const char* aEntry, bool makePartFileName( const char* aEntry,
const STRING& aCategory, STRING* aPartName ); const STRING& aCategory, STRING* aPartName );
/**
* Function readSExpression
* reads an s-expression into aResult. Candidate for virtual function later.
*/
void readSExpression( STRING* aResult, const STRING& aNameSpec ) throw( IO_ERROR );
/** /**
* Function doOneDir * Function doOneDir
* loads part names [and categories] from a directory given by * loads part names [and categories] from a directory given by
...@@ -109,39 +120,35 @@ public: ...@@ -109,39 +120,35 @@ public:
//-----<LIB_SOURCE implementation functions >------------------------------ //-----<LIB_SOURCE implementation functions >------------------------------
void ReadPart( STRING* aResult, const STRING& aPartName, const STRING& aRev=StrEmpty ) void ReadPart( STRING* aResult, const STRING& aPartName, const STRING& aRev=StrEmpty )
throw( IO_ERROR ) throw( IO_ERROR );
{
}
void ReadParts( STRING_TOKS* aResults, const STRINGS& aPartNames ) void ReadParts( STRINGS* aResults, const STRINGS& aPartNames )
throw( IO_ERROR ) throw( IO_ERROR );
{
}
void GetCategories( STRING_TOKS* aResults ) throw( IO_ERROR ) void GetCategories( STRINGS* aResults ) throw( IO_ERROR );
{
}
void GetCategoricalPartNames( STRING_TOKS* aResults, void GetCategoricalPartNames( STRINGS* aResults, const STRING& aCategory=StrEmpty )
const STRING& aCategory=StrEmpty ) throw( IO_ERROR ) throw( IO_ERROR );
{
}
void GetRevisions( STRING_TOKS* aResults, const STRING& aPartName ) throw( IO_ERROR ) void GetRevisions( STRINGS* aResults, const STRING& aPartName ) throw( IO_ERROR )
{ {
// @todo
} }
void FindParts( STRING_TOKS* aResults, const STRING& aQuery ) throw( IO_ERROR ) void FindParts( STRINGS* aResults, const STRING& aQuery ) throw( IO_ERROR )
{ {
// @todo
} }
//-----</LIB_SOURCE implementation functions >------------------------------ //-----</LIB_SOURCE implementation functions >------------------------------
#if defined(DEBUG)
/** /**
* Function Show * Function Show
* will output a debug dump of contents. * will output a debug dump of contents.
*/ */
void Show(); void Show();
#endif
}; };
} // namespace SCH } // namespace SCH
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
typedef std::string STRING; typedef std::string STRING;
typedef std::deque<STRING> STRINGS; typedef std::deque<STRING> STRINGS;
typedef STRINGS STRING_TOKS; typedef STRINGS STRINGS;
extern const STRING StrEmpty; extern const STRING StrEmpty;
...@@ -93,14 +93,14 @@ protected: ///< derived classes must implement ...@@ -93,14 +93,14 @@ protected: ///< derived classes must implement
* @param aPartNames is a list of part names, one name per list element. * @param aPartNames is a list of part names, one name per list element.
* @param aResults receives the s-expressions * @param aResults receives the s-expressions
*/ */
virtual void ReadParts( STRING_TOKS* aResults, const STRINGS& aPartNames ) virtual void ReadParts( STRINGS* aResults, const STRINGS& aPartNames )
throw( IO_ERROR ) = 0; throw( IO_ERROR ) = 0;
/** /**
* Function GetCategories * Function GetCategories
* fetches all categories present in the library source into @a aResults * fetches all categories present in the library source into @a aResults
*/ */
virtual void GetCategories( STRING_TOKS* aResults ) virtual void GetCategories( STRINGS* aResults )
throw( IO_ERROR ) = 0; throw( IO_ERROR ) = 0;
/** /**
...@@ -112,7 +112,7 @@ protected: ///< derived classes must implement ...@@ -112,7 +112,7 @@ protected: ///< derived classes must implement
* *
* @param aResults is a place to put the fetched result, one category per STRING. * @param aResults is a place to put the fetched result, one category per STRING.
*/ */
virtual void GetCategoricalPartNames( STRING_TOKS* aResults, const STRING& aCategory=StrEmpty ) virtual void GetCategoricalPartNames( STRINGS* aResults, const STRING& aCategory=StrEmpty )
throw( IO_ERROR ) = 0; throw( IO_ERROR ) = 0;
/** /**
...@@ -120,7 +120,7 @@ protected: ///< derived classes must implement ...@@ -120,7 +120,7 @@ protected: ///< derived classes must implement
* fetches all revisions for @a aPartName into @a aResults. Revisions are strings * fetches all revisions for @a aPartName into @a aResults. Revisions are strings
* like "rev12", "rev279", and are library source agnostic. These * like "rev12", "rev279", and are library source agnostic. These
*/ */
virtual void GetRevisions( STRING_TOKS* aResults, const STRING& aPartName ) virtual void GetRevisions( STRINGS* aResults, const STRING& aPartName )
throw( IO_ERROR ) = 0; throw( IO_ERROR ) = 0;
/** /**
...@@ -139,7 +139,7 @@ protected: ///< derived classes must implement ...@@ -139,7 +139,7 @@ protected: ///< derived classes must implement
* *
* @param aResults is a place to put the fetched part names, one part per STRING. * @param aResults is a place to put the fetched part names, one part per STRING.
*/ */
virtual void FindParts( STRING_TOKS* aResults, const STRING& aQuery ) virtual void FindParts( STRINGS* aResults, const STRING& aQuery )
throw( IO_ERROR ) = 0; throw( IO_ERROR ) = 0;
//-----</abstract for implementors>-------------------------------------- //-----</abstract for implementors>--------------------------------------
......
# This is a CMake toolchain file for ARM:
# http://vtk.org/Wiki/CMake_Cross_Compiling
# usage
# cmake -DCMAKE_TOOLCHAIN_FILE=../../toolchain-mingw.cmake ..
# It is here to assist Dick with verifying compilation of /new stuff with mingw (under linux)
set( CMAKE_SYSTEM_NAME Linux )
# Specific to Dick's machine, again for testing only:
include_directories( /svn/wxWidgets/include )
#-----<configuration>-----------------------------------------------
# configure only the lines within this <configure> block, typically
# specify the cross compiler
set( CMAKE_C_COMPILER i586-mingw32msvc-gcc )
set( CMAKE_CXX_COMPILER i586-mingw32msvc-g++ )
# where is the target environment
set( CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc )
#-----</configuration>-----------------------------------------------
# search for programs in the build host directories
set( CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER )
# for libraries and headers in the target directories
set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY )
set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY )
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment