Part of the EllisLab Network
   
1 of 2
1
Simple JavaScript database
Posted: 30 July 2007 10:43 AM   [ Ignore ]  
Summer Student
Total Posts:  12
Joined  07-30-2007

Hi, everyone.

I just created a very simple JavaScript database which I will be releasing under the GPL pending your review.  I just wanted to show it to someone before releasing it to the public at large.

It is a very simple object, and should be fairly simple to use.

Let me know if you see any way to optimize or otherwise make it better.

There are 2 functions preceding the obect:  clone() and print_r().  print_r() is only used in the examples.  clone() is actually used by the database.

and then there are a plethora of examples of usage following the object.  tweak and play around with those to your hearts content.

Basically, the object supports 4 functions (methods): _insert, _select, _update, _delete.

Each of the functions’ only argument should be an object (i.e. an associative array) whose members are arrays (or strings, depending on the case).  In the case of _insert, the object should only have one array: ‘records’ (which is an array of objects itself) which are taken and inserted.

_update, _select, and _delete all take a “criteria” object which can have the following members:
‘records’    ,
‘records_ne’  , (not equal)
‘records_lt’  , (less than)
‘records_gt’  , (greater than)
‘limit’    ,
‘limit_start’ ,
‘order_by’  , (field name)
‘order_dir’  , (asc or desc)

The ‘records/_ne|_lt|_gt’ are all an array of objects, which objects should follow the format
{‘name’:‘database field name’, ‘value’:‘value to search for’}.  These objects are like the SQL WHERE clause.  There is currently no functionality for OR or LIKE or pattern matching.

‘records’ in this context can actually be a string with the value of “*”, saying to search for all records.  If present, all other ‘record_’ members are ignored.

‘limit’ and ‘limit_start’ should be integers, and correspond to the SQL LIMIT x,x statement (LIMIT limit_start, limit).

‘order_by’ and ‘order_dir’ correspond to the SQL ORDER statement (ORDER BY order_by order_dir).

At any rate, take a look at it, and let me know what you think, before I release it.

———————————-
Michael
———————————-

/**
* clone
*
* @param obj $obj
* @access public
* @return void
*/
function clone(obj){
    
if(obj == null || typeof(obj) != 'object')
        return
obj;

    if (
obj.reverse ){ // array
        
var temp = new Array();
        for(var
key in obj)
            
temp[key] = clone(obj[key]);
        return
temp;
    
}

    
var temp = {}; // object
    
for(var key in obj)
        
temp[key] = clone(obj[key]);

    return
temp;
}

/**
* print_r
*
* prints an array or object into readable format.
*
* I actually took most of this function from:
* http://www.phpbuilder.com/board/archive/index.php/t-10294264.html
*
* and modified it a little to fit my liking ( such as fiddling
* with the _indent and adding a 'function' case statement ).
*
* @param input $input
* @param _indent $_indent
* @access public
* @return void
*/
function print_r(input, _indent)
{
    
if(typeof(_indent) == 'string') {
        
var indent = _indent + _indent;
        var
paren_indent = _indent;
    
} else {
        
var indent = '    ';
        var
paren_indent = ' ';
    
}
    
switch(typeof(input)) {
        
case 'boolean':
            var
output = (input ? 'true' : 'false') + "\n";
            break;
        case
'object':
            if (
input === null ) {
                
var output = "null\n";
                break;
            
}
            
var output = ((input.reverse) ? 'Array' : 'Object') + " (\n";
            for(var
i in input) {
                output
+= indent + "[" + i + "] => " + print_r(input[i], indent);
            
}
            output
+= paren_indent + ")\n";
            break;
        case
'string':
            var
output = '"' + input + "\"\n";
            break;
        case 'function':
            if ( input.toString().indexOf('{') != -1 ){
                var output = input.toString().substr(0, input.toString().indexOf('{') + 1) + "
}\n";
            }
            else {
                var output = input + "
\n";
            }
            break;
        case 'number':
        default:
            var output = "" + input + "
\n";
    }
    return output;
}
Profile
 
 
Posted: 30 July 2007 10:45 AM   [ Ignore ]   [ # 1 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007
/**
*  js_database
*
*  @author Michael Bahnmiller
*  @author Ezekiel Messenger
*/
var js_database = function() {
    this
.records = new Array();
    
this.temprecords = new Array();

    
this.sortfield = '';
    var
me = this;

    
/**
     *  _select
     *
     *  @param mixed criteria
     */
    
this._select = function(criteria)
    
{
        
var limit = criteria.limit;
        var
limit_start = (criteria.limit_start && typeof(criteria.limit_start) == 'number') ? criteria.limit_start : 0;

        if (
typeof(criteria.records) == 'string' ){
            
if ( criteria.records != '*' )
                return
false;

            if (
limit == 0 ){
                
return new Array();
            
}

            this
.__doSort(criteria);

            if (
limit > 0 && limit < this.temprecords.length ) {
                
if ( limit_start > 0 )
                    
this.temprecords.splice(0, limit_start);
                
this.temprecords.splice(limit, this.temprecords.length - limit);
                return
{"records" : this.temprecords, "count" : this.temprecords.length};
            
}

            
return {"records" : this.temprecords, "count" : this.temprecords.length};
        
}

        
var c = 0;
        var
d = 0;
        var
num_matches = 0;
        var
ret_data_Obj = {"records" : [], "count" : ""};

        var
len = 0;
        if (
criteria.records)    len += parseInt(criteria.records.length);
        if (
criteria.records_gt) len += parseInt(criteria.records_gt.length);
        if (
criteria.records_lt) len += parseInt(criteria.records_lt.length);
        if (
criteria.records_ne) len += parseInt(criteria.records_ne.length);

        
this.__doSort(criteria);

        for (
i in this.temprecords){
            c
= 0;
            for(
j in criteria){
                
if ( j == 'records' || j == 'records_lt' || j == 'records_gt' || j == 'records_ne' ) {
                    
for(k in criteria[j]) {
                        
if ( j == 'records' ){
                            
if ( this.temprecords[i][criteria.records[k].name] == criteria.records[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_lt' ){
                            
if ( this.temprecords[i][criteria.records_lt[k].name] < criteria.records_lt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_gt' ){
                            
if ( this.temprecords[i][criteria.records_gt[k].name] > criteria.records_gt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_ne' ){
                            
if ( this.temprecords[i][criteria.records_ne[k].name] != criteria.records_ne[k].value )
                                
c++;
                            else break;
                        
}

                        
if ( c == len ){
                            num_matches
++;
                            if (
num_matches > limit_start ) {
                                d
++;
                                
ret_data_Obj.records.push(this.temprecords[i]);
                            
}

                            
if ( d == parseInt(limit) ){
                                ret_data_Obj
.count = num_matches;
                                return
ret_data_Obj;
                            
}
                        }
                    }
                }
            }
        }

        ret_data_Obj
.count = num_matches;
        return
ret_data_Obj;
    
};

    
/**
     *  _insert
     *
     *  @param array rows
     */
    
this._insert = function(rows)
    
{
        
for(i in rows.records){
            this
.records[this.records.length] = rows.records[i];
        
}
    }
;
Profile
 
 
Posted: 30 July 2007 10:46 AM   [ Ignore ]   [ # 2 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007
/**
     *  _delete
     *
     *  @param array criteria
     */
    
this._delete = function(criteria)
    
{
        
var limit = criteria.limit;
        var
limit_start = (criteria.limit_start && typeof(criteria.limit_start) == 'number') ? criteria.limit_start : 0;

        var
c = 0;
        var
d = 0;
        var
num_matches = 0;
        var
ret_data_Obj = {"records" : [], "count" : ""};
        var
ids_to_del = new Array();

        var
len = 0;
        if (
criteria.records)    len += parseInt(criteria.records.length);
        if (
criteria.records_gt) len += parseInt(criteria.records_gt.length);
        if (
criteria.records_lt) len += parseInt(criteria.records_lt.length);
        if (
criteria.records_ne) len += parseInt(criteria.records_ne.length);

        
this.__doSort(criteria, true);

        for (
i in this.records){
            c
= 0;
            for(
j in criteria){
                
if ( j == 'records' || j == 'records_lt' || j == 'records_gt' || j == 'records_ne' ) {
                    
for(k in criteria[j]) {
                        
if ( j == 'records' ){
                            
if ( this.records[i][criteria.records[k].name] == criteria.records[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_lt' ){
                            
if ( this.records[i][criteria.records_lt[k].name] < criteria.records_lt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_gt' ){
                            
if ( this.records[i][criteria.records_gt[k].name] > criteria.records_gt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_ne' ){
                            
if ( this.records[i][criteria.records_ne[k].name] != criteria.records_ne[k].value )
                                
c++;
                            else break;
                        
}

                        
if ( c == len ){
                            num_matches
++;
                            if (
num_matches > limit_start ) {
                                d
++;
                                
ret_data_Obj.records.push(this.records[i]);
                                
ids_to_del[ids_to_del.length] = i;
                            
}
                        }

                        
if ( d == parseInt(limit) ) break;
                    
}
                }
            }

            
if ( d == parseInt(limit) ) break;
        
}

        ids_to_del
.reverse();
        for(
k in ids_to_del) {
            this
.records.splice(ids_to_del[k], 1);
        
}

        ret_data_Obj
.count = num_matches;
        return
ret_data_Obj;
    
};
Profile
 
 
Posted: 30 July 2007 10:47 AM   [ Ignore ]   [ # 3 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007
/**
     *  _update
     *
     *  @param array criteria
     */
    
this._update = function(criteria)
    
{
        
var limit = criteria.limit;
        var
limit_start = (criteria.limit_start && typeof(criteria.limit_start) == 'number') ? criteria.limit_start : 0;

        
this.__doSort(criteria, true);

        var
c = 0;
        var
num_matches = 0;
        var
ret_data_Obj = {"records" : [], "count" : ""};

        if (
typeof(criteria.records) == 'string' ){
            
if ( criteria.records != '*' )
                return
false;

            for (
i in this.records){
                
if ( c >= limit_start ) {
                    
for (j in criteria.change){

                        this
.records[i][criteria.change[j].name] = criteria.change[j].value;
                        
ret_data_Obj.records.push(this.records[i]);
                    
}
                    num_matches
++;
                
}

                c
++;
                if (
c == parseInt(limit + limit_start) ){
                    ret_data_Obj
.count = num_matches;
                    return
ret_data_Obj;
                
}
            }

            ret_data_Obj
.count = num_matches;
            return
ret_data_Obj;
        
}

        
var d = 0;
        var
len = 0;

        if (
criteria.records)    len += parseInt(criteria.records.length);
        if (
criteria.records_gt) len += parseInt(criteria.records_gt.length);
        if (
criteria.records_lt) len += parseInt(criteria.records_lt.length);
        if (
criteria.records_ne) len += parseInt(criteria.records_ne.length);


        for (
i in this.records){
            c
= 0;
            for(
j in criteria){
                
if ( j == 'records' || j == 'records_lt' || j == 'records_gt' || j == 'records_ne' ) {
                    
for(k in criteria[j]) {
                        
if ( j == 'records' ){
                            
if ( this.records[i][criteria.records[k].name] == criteria.records[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_lt' ){
                            
if ( this.records[i][criteria.records_lt[k].name] < criteria.records_lt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_gt' ){
                            
if ( this.records[i][criteria.records_gt[k].name] > criteria.records_gt[k].value )
                                
c++;
                            else break;
                        
} else if ( j == 'records_ne' ){
                            
if ( this.records[i][criteria.records_ne[k].name] != criteria.records_ne[k].value )
                                
c++;
                            else break;
                        
}

                        
if ( c == len ){
                            num_matches
++;
                            if (
num_matches > limit_start ) {
                                d
++;

                                for (
k in criteria.change){
                                    this
.records[i][criteria.change[k].name] = criteria.change[k].value;
                                
}

                                ret_data_Obj
.records.push(this.records[i]);
                            
}

                            
if ( d == parseInt(limit) ){
                                ret_data_Obj
.count = num_matches;
                                return
ret_data_Obj;
                            
}
                        }
                    }
                }
            }
        }

        ret_data_Obj
.count = num_matches;
        return
ret_data_Obj;
    
};

    
/**
     *  __sort
     *
     * @access private
     * @param a
     * @param b
     */
    
this.__sort = function(a, b)
    
{
        
if ( ! (a[me.sortfield] && b[me.sortfield]) )
            return
0;

        if (
typeof(a[me.sortfield]) == 'number' && typeof(b[me.sortfield]) == 'number' ){
            
return a[me.sortfield] - b[me.sortfield];
        
} else {
            
var x = a[me.sortfield].toLowerCase();
            var
y = b[me.sortfield].toLowerCase();
            return ((
x<y)?-1:((x>y)?1:0));
        
}
    }

    
/**
     *  __doSort
     *
     * @access private
     * @param criteria
     */
    
this.__doSort = function(criteria, sort_main)
    
{
        
if ( sort_main === true ){

             
var var_to_sort = 'records';
        
} else {

            
var var_to_sort = 'temprecords';

            
this.temprecords = new Array();
            
this.temprecords = clone(this.records);
        
}

        
if ( !criteria.order_by ){
            
if ( !criteria.order_dir || criteria.order_dir != 'desc')
                return;

            
this[var_to_sort].reverse();
            return;
        
}

        this
.sortfield = criteria.order_by;
        
this[var_to_sort].sort(this.__sort);

        if (
criteria.order_dir && criteria.order_dir == 'desc' )
            
this[var_to_sort].reverse();
    
}
}
Profile
 
 
Posted: 30 July 2007 10:50 AM   [ Ignore ]   [ # 4 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007
///////////////////////////////////////////////////////////////////////////
// EXAMPLES OF USAGE:
///////////////////////////////////////////////////////////////////////////

    //-----------------------------
    //-----------------------------
    // Create a new database
    //
        
newdb = new js_database();
    
//
    //-----------------------------
    //-----------------------------

    //--------------------------------------------------------------------
    //--------------------------------------------------------------------
    // INSERT records into the new database
    //
        
insert = {
            
'records' : [
                {
'name':'Fred' , 'occupation':'doctor' , 'children':2  },
                
{'name':'Jim'  , 'occupation':'doctor' , 'children':4  },
                
{'name':'Bob'  , 'occupation':'pastor' , 'children':2  },
                
{'name':'Mary' , 'occupation':'nurse'  , 'children':3  }
            ]
        }
;

        
alert( 'NEWDB: '     + print_r(newdb ) );
        
alert( 'INSERTING: ' + print_r(insert) );
        
newdb._insert(insert);

        
alert( 'NEWDB: '    + print_r(newdb) );
        
alert( 'SELECT *: ' + print_r(newdb._select({'records':'*'})));
    
//
    //--------------------------------------------------------------------
    //--------------------------------------------------------------------

    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // INSERT _additional_ records into the database
    //
        
insert = {
            
'records' : [
                {
'name':'Kami' , 'occupation':'homemaker'  , 'children':2  },
                
{'name':'Mike' , 'occupation':'programmer' , 'children':2  },
                
{'name':'Mike' , 'occupation':'super dude' , 'children':12 }
            ]
        }
;

        
alert( 'INSERTING: ' + print_r(insert ) );
        
newdb._insert(insert);

        
alert( 'NEWDB: '    + print_r(newdb) );
        
alert( 'SELECT *: ' + print_r(newdb._select({'records':'*'})));
    
//
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------

    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // DELETE from the database (with various criteria
    //
        
del = {
            
//'records' : '*',
            
'records' : [
                
//{'name':'occupation', 'value':'doctor'}
            
],
            
'records_gt' : [
                
//{'name':'children', 'value':2}
            
],
            
'records_lt' : [
                
//{'name':'children', 'value':4}
            
],
            
'records_ne' : [
                
//{'name':'name', 'value':'Bob'}
            
]
        }
;

        
del.limit       = 2;      // with a limit
        
del.limit_start = 1;      // with a limit
        
del.order_by    = 'name'; // with an ORDER BY
        
del.order_dir   = 'desc'; // with an order by direction

        
alert( 'DELETING: ' + print_r(del) );
        
deleted = newdb._delete(del);
        
alert( 'DELETED: ' + print_r(deleted) );

        
alert( 'SELECT *: ' + print_r(newdb._select({'records':'*'})));
    
//
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------

    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // perform SELECTs with various criteria
    //
        
select = {
            
//'records' : '*',
            
'records' : [
                
//{'name':'occupation', 'value':'doctor'}
            
],
            
'records_gt' : [
                
//{'name':'children', 'value':3}
            
],
            
'records_lt' : [
                
//{'name':'children', 'value':4}
            
],
            
'records_ne' : [
                
//{'name':'children', 'value':2}
            
]
        }
;
        
select.limit       = 2;      // with a limit
        
select.limit_start = 1;      // with a limit start
        
select.order_by    = 'name'; // with an ORDER BY
        
select.order_dir   = 'asc';  // with an order by direction
        
alert( 'SELECTING: ' + print_r(select) );
        
recs = newdb._select(select);
        
alert( 'SELECTED: ' + print_r(recs) );

        
select.limit       = 2;      // with a limit
        
select.limit_start = 1;      // with a limit start
        
select.order_by    = 'name'; // with an ORDER BY
        
select.order_dir   = 'asc';  // with an order by direction
        
alert( 'SELECTING: ' + print_r(select) );
        
recs = newdb._select(select);
        
//alert( 'SELECTED: ' + print_r(recs) );
    //
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------

    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // yet another SELECT
    //
        
select = {
            
'records' : [
                {
'name':'children'   , 'value':'2'},
                
{'name':'occupation' , 'value':'homemaker'}
            ]
        }
;
        
alert( 'SELECTING: ' + print_r(select) );
        
recs = newdb._select(select);
        
alert( 'SELECTED: ' + print_r(recs) );
    
//
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
Profile
 
 
Posted: 30 July 2007 10:50 AM   [ Ignore ]   [ # 5 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007
//------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // let's try an UPDATE on _everything_
    //
        
update = {
            
'records' : '*',
            
'change' : [
                {
'name':'new_field', 'value':'new_value'}
            ]
        }
        
//alert( 'UPDATING: ' + print_r(update) );
        
updated = newdb._update(update);
        
//alert( 'UPDATED: '  + print_r(updated) );
        //alert( 'SELECT *: ' + print_r(newdb._select({'records':'*'})));
    //
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------




    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
    // an UPDATE with various criteria
    //
        
update = {
            
//'records' : '*',
            
'records' : [
                
//{'name':'name', 'value':'Mike'},
                //{'name':'children' , 'value':'2'}

                //{'name':'occupation' , 'value':'doctor'}
            
],
            
'records_gt' : [
                
//{'name':'children', 'value':2}

                //{'name':'name', 'value':'Mike'}
            
],
            
'records_lt' : [
                
//{'name':'children', 'value':4}
            
],
            
'records_ne' : [
                
//{'name':'children', 'value':2}
            
],
            
'change' : [
                
//{'name':'desired_children', 'value':'12'}//,
                //{'name':'name', 'value':'I am Cool'}
            
]
        }

        
//update.limit       = 3;      // with a limit
        //update.limit_start = 2;      // with a limit_start
        //update.order_by    = 'name'; // with an ORDER BY
        //update.order_dir   = 'asc';  // with an order by direction
        //alert( 'UPDATING: ' + print_r(update) );
        
updated = newdb._update(update);
        
//alert( 'UPDATED: '  + print_r(updated) );
        //alert( 'SELECT *: ' + print_r(newdb._select({'records':'*'})));
    //
    //------------------------------------------------------------------------
    //------------------------------------------------------------------------
Profile
 
 
Posted: 30 July 2007 10:55 AM   [ Ignore ]   [ # 6 ]  
Grad Student
Rank
Total Posts:  78
Joined  05-14-2007

Very nice.  Thanks Cyclops I will test it out.

Profile
 
 
Posted: 31 July 2007 12:19 AM   [ Ignore ]   [ # 7 ]  
Summer Student
Total Posts:  8
Joined  05-03-2007

This is very impressive.  Do you know what the record limit is?

Profile
 
 
Posted: 31 July 2007 11:37 AM   [ Ignore ]   [ # 8 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007

I sure don’t.  However many objects a JavaScript array will hold, I imagine.  There is no limit inherent in the database engine itself.

Thanks for the complement.  Feel free to use it anywhere you want.

Profile
 
 
Posted: 31 July 2007 11:42 AM   [ Ignore ]   [ # 9 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007

Oh… I forgot to mention in the documentation that any time an ORDER BY is performed, the actual contents of the database are changed (not just the return or the search), so that if you do a _select, _update, or _delete and add an order_by, the actual this.records array of the database is reordered.

Profile
 
 
Posted: 10 August 2007 05:48 PM   [ Ignore ]   [ # 10 ]  
Summer Student
Total Posts:  2
Joined  08-10-2007

Can you please post an html example, a sample in use?

Profile
 
 
Posted: 10 August 2007 06:27 PM   [ Ignore ]   [ # 11 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007

Well, actually I haven’t used it for anything yet.  Simply did testing on it.  If you can come up with an idea for me, maybe I can throw together an example.  Maybe something where you make a call with AJAX to a Web Service that returns XML that you could parse and throw into the database.  You could then hook the database up to a datagrid.  (Maybe someone should develop that… a datagrid that will work off the database…)

Or you could start with an empty datagrid, and users can enter information into a form and after each “record” they enter, it populates both the database and the datagrid.  The user could delete records, update them, etc… and then when they click “save” you could send the data from the database through a web service to be saved in a MySQL database…

Do you have any other ideas?  If you come up with something simple, I could whip together an example!

Profile
 
 
Posted: 10 August 2007 07:01 PM   [ Ignore ]   [ # 12 ]  
Grad Student
Rank
Total Posts:  78
Joined  05-14-2007

One DataGrid coming up!

Profile
 
 
Posted: 10 August 2007 07:07 PM   [ Ignore ]   [ # 13 ]  
Grad Student
Rank
Total Posts:  78
Joined  05-14-2007

Cyclops you need to host this somewhere and post a link so us lazies don’t have to copy and paste multiple pages.

Profile
 
 
Posted: 10 August 2007 07:08 PM   [ Ignore ]   [ # 14 ]  
Summer Student
Total Posts:  12
Joined  07-30-2007

You got any thoughts, usmc, on where I might could?

Profile
 
 
Posted: 10 August 2007 07:32 PM   [ Ignore ]   [ # 15 ]  
Grad Student
Rank
Total Posts:  78
Joined  05-14-2007

I have hosted it here

http://www.storageinstructions.com/samples/js_database.js

Profile
 
 
   
1 of 2
1
 
Post Marker Legend
New Topic New posts Hot Topic Hot Topic with new posts New Poll New Poll Moved Topic Moved Topic Sticky Topic Sticky topic
Old Topic No new posts Hot Old Topic Hot Topic with no new posts Old Poll Old Poll Closed Topic Closed Topic Announcement Announcements
Theme
Change Theme
Visitor Statistics
The most visitors ever was 721, on January 06, 2010 09:38 AM
Total Registered Members: 115007 Total Logged-in Users: 62
Total Topics: 122440 Total Anonymous Users: 4
Total Replies: 647313 Total Guests: 500
Total Posts: 769753    
Members ( View Memberlist )