PDA

نسخه کامل مشاهده نسخه کامل : JDBC Scripting به چه معناست؟



Babak_King
05-12-2005, 11:54
JudoScript يك زبان نوشتاري جاوا است كه ساده و قدرتمند بوده و پشتيبان تابعي مختص دامنه 4GL-Link (domain-specific) را دارد و به كاربران اجازه مي‌دهد، مشخص كنند چه كاري مي‌خواهند انجام دهند و چه زماني برنامه‌هاي الگوریتمي لازم مي‌باشند.

اين پشتيبان تابعي، جودواسکریپت را در يك دسته جديد از زبانهاي نوشتاري تابعي قرار مي‌دهد، مانند هر زبان نسل چهارم. جودواسکریپت روي مشخص كردن اهداف و آماده‌سازي راه‌حل‌هاي خودكار (الگوريتم‌ها)، براي عملي كردن اهداف، تاكيد دارد. جودواسکریپت از نحوی شبيه نحو جاوااسکریپت و مدل برنامه‌نويسي در آن استفاده مي‌كند، ولي قدرت محاسبه بيشتري در ساختار داده‌هاي كامل دارد. اين زبان همچنين در الگوهاي جاوا بسيار قوي است، بنابراين به همه منابع قابل خواندن جاوا، دسترسي دارد. هماهنگي تمام اين خصيصه‌ها باعث ايجاد قدرت، بهره‌وري و ظرافت زيادي در برآوردن نيازها شده است.

پردازش داده يكي از مهمترين كاربردهاي جودواسکریپت است. امروز برنامه‌هاي كاربردي با فرمتهاي داده‌اي خاص و كامل، مثل پايگاه داده‌هاي رابطه‌اي XML، (Standard Generalized Markup Language) SGML، انواع داده‌هاي انتزاعي مثل EJBها (Enterprise JavaBeans)، سرويس‌هاي وب و فايلهاي مسطح كار مي‌كنند.

جودواسکریپت براي پردازش داده‌اي چند فرمتي و چند منبعی كاملا ايده‌آل است. اين دو بخش روي (Java Database Connectivity) JDBC جودواسکریپت تمركز مي‌كنند، زبان جودواسکریپت را معرفي مي‌‌نمايند و پردازش داده با J2EE را شرح مي‌دهد. بخش اول پشتيباني نوشتاري JDBC جودواسكريپت را شرح مي‌دهد.



JDBC Scripting به چه معناست؟

JDBC يك استاندارد APIجاوا براي دستيابي به پايگاه‌هاي داده‌ SQL است. از JDBC براي دستيابي به پايگاه داده در جهت برطرف كردن نيازهاي مشتريان پايگاه، استفاده مي‌شود. نكته قابل توجه اينجاست كه شركت‌هاي اصلي (Relational Database Management System) RDBMS دستورالعمل‌هاي JDBC خالص را براي توليدات خودشان تهيه مي‌كنند، بنابراين يك ابزار مفيد اسكريپت‌نويسي JDBC مي‌تواند به سادگي امتيازي از اين قابليت را بگيرد. تمام چيزي كه نياز داريد، JDBC URL و فايل JDBC درايور مي‌باشد كه در classpath وجود دارد. شما مي‌توانيد همزمان به چند پايگاه داده براي پردازش داده ناهمگن وصل شويد.

از نظر فلسفي، اسكريپت‌نويسي JDBC بخشي از يك ايده بزرگ است: استفاده از پايه جاوا به عنوان يك زبان شي‌گرا و براي اجراي منظم محاسبه كارها. زبان جودواسکریپت به اين منظور طراحي گرديد.

امروزه شركتهاي بزرگ نرم‌افزاري فقط از پايگاه داده‌هاي رابطه‌اي استفاده نمي‌كنند و قالب داده‌هاي قوي‌تر مثل XML وSGML و انواع داده‌هاي انتزاعي مثل Enterprise EJB و سرويس‌هاي وب به خوبي همان فرمت‌هاي قديمي مثل فايل‌هاي هموار و صفحات گسترده عمل مي‌كنند.

اكنون ابزارهاي مختلفي را براي پردازش، گزارش گيري و پرس‌وجوي داده‌ها به كار مي‌برند كه نمونه‌هايي از مزيت‌هاي بديهي آنها به صورت ذيل مي‌باشد:

1. شما مي‌توانيد هر پردازشي را با هر منبع داده‌اي با هر فرمتي به صورت همزمان انجام دهيد.

2. دسترسي به راه‌حل‌ها و نتايج درست، سريعتر است

3. ابزارهاي كمي براي يادگيري، نصب، پيكربندي و راه‌اندازي مورد نياز است

4. دارا بودن يك ابزار رايگان و منفرد ارزانتر تمام مي‌شود

5. فرآيند حل مشكل آسانتر مي‌باشد، زيرا مي‌توانيد بدون نگراني در مورد مسائل محيطي، تعويض زمينه (context switching) و يكپارچگي كامپوننت‌هاي نامتجانس مشكل را بررسي كنيد.

جاوا در حد يك ابزار عملي تنزل كرده و اين بدان علت است كه به عنوان يك زبان سيستمي، براي ايجاد سيستم‌هاي نرم‌افزاري شي‌گرا طراحي شده است.

يك وسيله نوشتاري خوب بايد خصيصه‌هاي تابعي انتزاعي براي كاربران و براي بهتر انجام شدن كارها داشته باشد و همچنين قدرت برنامه‌نويسي آن كم نشود.

از طرفي SQL (محض) خيلي محدود شده، چرا كه ابزارهاي پرس‌وجو ساده خيلي ضعيف عمل مي‌كنند. جودواسکریپت ساختارهاي داده‌اي زيادي دارد و براي كامپوننت‌هاي جاوا خيلي قوي است. شما مي‌توانيد از جودواسکریپت براي دريافت داده از پايگاه داده A، محاسبه برخي نتايج مياني در حافظه و پايان كار با پايگاه داده B، استفاده كنيد. اگر از اوراكل استفاده مي‌كنيد، حتما نياز بر استفاده از PL/SQL، پيوندهاي پايگاه داده و جداول موقت خواهيد داشت. جودواسکریپت به راحتي مي‌تواند با فايل‌هاي ساده، صفحات گسترده، XML، SGML، EJBS و غيره كار كند.



پشتيباني JDBC در جودواسكريپت:

جودواسکریپت مجموعه‌اي از پشتيبان‌هاي نحوي را براي اجراي SQL دارد. دستورات زبان تعريف داده (DDL) و زبان دستكاري داده (DML) مي‌توانند به صورت منفرد يا به صورت گروهي يا دسته‌اي اجرا شوند. شما مي‌توانيد پروسيجرها را (روال) با پارامترهاي -in، -out و in-out فراخواني كنيد. مثال ساده زير را ببينيد:




connect to 'jdbc:oracle:thin:@dbsvr:1521:dbname', 'user', 'pass';

// Create table and insert a few rows.
executeSQL {
CREATE TABLE emp(emp_no INTEGER PRIMARY KEY,

first_name VARCHAR(100),
last_name VARCHAR(100),
birth_date DATE,
salary NUMBER);
CREATE INDEX emp_fname ON emp(first_name);
CREATE INDEX emp_lname ON emp(last_name);

INSERT INTO emp(emp_no,first_name,last_name,birth_date,salary)
VALUES(100, 'Jim', 'Billups', to_date('1954-1-3','yyyy-mm-dd'), 86500.0);

INSERT INTO emp(emp_no,first_name,last_name,birth_date,salary)
VALUES(101, 'Linda', 'Jordan', to_date('1980-7-24','yyyy-mm-dd'),45250.0);
}

// Query and print out rows.
executeQuery qry:
SELECT emp_no, first_name, last_name, salary
FROM emp
WHERE salary < 50000
ORDER BY salary ASC
;
while qry.next() {
println '#', qry[1], ' ', qry.last_name, ', ', qry.first_name, ': ',
qry.salary;
}

disconnect(); // From database



اين برنامه به پايگاه داده وصل شده، تعدادي از دستورات تعريف و دستكاري داده SQL را اجرا كرده و نهايتا يك پرس‌وجو را براي چاپ نتايج در جدول اجرا مي‌كند. در بخش executeQuery، متغير qry بخشي ازJava.Sql.ResultSet است، پس شما مي‌توانيد متد next() را فراخواني كنيد، ستون‌ها در يك سطر مي‌توانند با اسامي يا شاخص‌هايشان مورد دستيابي قرار گيرند. مثال بعدي نشان مي‌دهد كه چطور متغيرها مي‌توانند محدود و مقيد (bound) شوند: در اين مثال بخش‌هاي مربوط به اتصال به پايگاه داده حذف شده است:


// Prepare a SQL
prepare qry:
SELECT emp_no, first_name, last_name, salary
FROM emp
WHERE salary < ?
ORDER BY salary ASC
;

// Run the query
executeQuery qry with @1:number = 5000.0;
while qry.next() {
println '#', qry[1], ' ', qry.last_name, ', ', qry.first_name, ': ',
qry.salary;
}

در كد بالا، ما از متد toCsv()آرايه‌ها استفاده كرده و يك تابع‌ بي‌نام را براي متغيرها به كار مي‌بريم. نتيجه عبارت SQL به اين صورت است:


SELESCT * FROM emp WHERE Last_ name IN (‘Olajuwan’, ‘Yao’).

مثالهايي را ديديم كه عبارت‌هاي SQL را به‌ طور خودكار ساخته و SQL را مستقيما اجرا مي‌كند. حالت ديگر از اجراي SQL به صورت دسته‌اي است.



اتصالات پايگاه‌هاي داده

شما به ‌صورت زير به يك پايگاه داده وصل مي‌شويد:


cannect mycon to ‘jdbc:oracle:thin:@dbsvr:1521:dbname’, ‘user’, ‘pass’;



اتصال ايجاد شده در متغير mycon ذخيره شده است. اگر نام متغير اتصال حذف شود، جودواسكريپت از متغير سراسري از پيش تعريف شده $$con استفاده مي‌كند. مي‌توانيد صفات اتصال را مثل زير مشخص كنيد:




connect mycon ( autoCommit=false ) to
'jdbc:oracle:thin:@dbsvr:1521:dbname', 'user', 'pass';



چطور جودواسكريپت، درايور JDBC را بارگذاري مي‌كند؟ جودواسکریپت ليستي از اسامي كلاس‌هاي درايور JDBC و پيشوندهاي URL آنها را دارد. مثلا وقتي جودواسكريپت، اوراكل را در Jdbc:oracle:… ببيند، كلاس درايور JDBC آن يعني oracle.jdbc.driver.JdbcDriver را بارگذاري مي‌كند. اگر يكي از درايورها در ليست نباشد، به عنوان يك صفت درايور مشخص مي‌شود يا به شيوه قديمي جاوا بارگذاري مي‌گردد:




// JudoScript style

connect (driver=‘my.db.jdbc.driver’) to 'jdbc:….’, “/”;

// Java style

(java::class). forName (‘my.db.jdbc.driver’);



يك شيء اتصال يك شيء java.Sql.Connection است. شرط use در تمام دستورات اجرايي Sql مشخص مي‌كند كه كدام اتصال بايد استفاده شود:


executeSQLuse mydb {…}



هنگامي كه عمليات پايگاه داده كامل مي‌شود، شما بايد متد disconnect() را فراخواني كنيد. تابع سيستمي disconnect() متد name-sake از شيء سراسري $$con را فراخواني مي‌كند، همچنين شيء اتصال، متدهاي زيادي مثل Commit() و rollback() دارد.


آماده سازي و اجرا
نحوه اجراي پرس‌وجوها و پردازش نتايج آنها .



اجراي پرس‌وجوها:

نحو عمومي در BNF (Backus Naur Form) براي executeQuery به صورت زير است:




executeQuery variable [ ( attributes ) ] [ use variable ] :
sql_statement ; [ with bind_list ; ] | executeQuery variable with bind_list ;



نحو اصلي شامل دو شكل از عبارت executeQuery است. شكل دوم يك عبارت ذخيره شده آماده در متغير را اجرا مي‌كند كه بعد توضيح داده مي‌شود. هر دو شكل مجموعه‌اي از نتايج را برمي‌گردانند. شرط use variable مشخص مي‌كند كه كدام اتصال پايگاه داده بايد استفاده شود، جايي كه variable يك شيء اتصال را نگه ‌مي‌دارد. SELECT يكي از بيشترين دستورات مورد استفاده در SQL است، اما فقط اين نيست. درايورهايJDBC زيادي از دستورات RDBMS خود پشتيباني مي‌كنند كه همگي مجموعه‌اي از نتايج را باز مي‌گردانند مثل دستورات Show در MySQL. اشيايي كه براي پرس‌وجو به كار مي‌روند نيز مجموعه‌اي از نتايج را باز مي‌گردانند. جودواسکریپت مقادير بازگشتي را به انواع داده‌اي در جودواسکریپت تبديل مي‌كند: يكي از پر استفاده‌ترين متدها، متد Next() است كه در ميان مجموعه نتايج، پيمايش مي‌كند.




executeQuery qry:
SELECT emp_no, first_name, last_name, birthday, salary
FROM emp
WHERE salary < 50000
ORDER BY salary ASC;
while qry.next() {
println '#', qry[1],' ',qry[2],', ',qry[3],
'(', qry[4].fmtDate('yyyy-MM-dd'), '): ', qry.salary;
}



در مثال بالا ستون اول يك عدد صحيح، ستون دوم و سوم رشته‌اي و ستون چهارم data و بعدي number است. چون آخرين ستون تاريخ است، مي‌توانيم متد fmtData() را فراخواني كنيم.



فراتر از SQL

قدرت اسكريپت‌نويسي JDBC فقط در اجراي دستورات SQL نيست، بلكه انجام محاسبات و SQL را نيز هماهنگ مي‌كند. فرض كنيد كه ثبت وقايع يك وب در جدولي شبيه زير ذخيره شده است:




CREATE TABLE web_log (
uri VARCHAR(1500),
referer VARCHAR(1500),
time TIMESTAMP
);



ما به گزارشي از تعداد وقايع در روز، در هفته و در ماه نياز داريم. گزارش روزانه و ماهانه با SQL امكان پذير است، اگر RDBMS توابع را در قسمت شرط GROUP BY پشتيباني كند، نظريه اين است كه زمان را به رشته تبديل كرده و در قسمت GROUP BY بكار ببريد. راه حل جودواسکریپت براي اين مساله به صورت زير است.


1: executeQuery qry: SELECT time from web_log;
2:
3: // Step 1. Collect weekly counts
4: counts = new Object;
5: while qry.next() {
6: time = qry.time;
7: token = time.year + '-week-';
8: if time.weekOfYear < 10 {
9: token += '0'; // Fill in a 0 for single digit numbers.
10: }
11: token += time.weekOfYear; // The week-token for the day.
12: ++ counts.(token);
13: }
14:
15: // Step 2. Print out weekly counts
16: for wk in counts.keysSorted() {
17: println wk, ':', counts.(wk) :>8; // Right-aligned, 8-digits
18: }



كليدهاي راه‌حل ساده اين مساله ساختار داده دروني Object و جنبه‌هاي مقادير Data مي‌باشند. Object ارسالي يك نگاشت است كه جفت نام و مقدار را ذخيره مي‌كند. در خط 12 و 16 gets و sets مقداري را به كليد نسبت مي‌دهند يا از آن برمي‌گردانند. در خطهاي 6 تا 11 يك نشانه هفتگي براي زمان مي‌سازيم و مقدار آن را در Object اضافه مي‌كنيم. اگر كليدي وجود نداشت، مقدار Null يا0 قرار داده مي‌شود. در خط 15 كليدهاي ذخيره شده را با همان ترتيب طبيعي بدست مي‌آوريم. نتايج شبيه كد زير مي‌باشند:




2004-week-06: 8438
2004-week-07: 21409
2004-week-08: 34940
2004-week-09: 128343
2004-week-10: 99827
2004-week-11: 78343
2004-week-12: 30968
2004-week-13: 44021



متدهاي نتايج پرس و جو

شيء پرس‌وجو متدهاي مفيدي دارد كه در جدول 2 نشان داده شده:
توضيح متد

ويژگيهاي ستونهاي پرس و جو را به عنوان يك شيءTableData برمي‌گرداند




getColumnAttributes ()

به اندازه limit سطر از نتيجه پرس و جو را برمي‌گرداند.. اگر تنها يك ستون در پرس و جو وجود داشته باشد، نتيجه يك آرايه است؛ در غير اينصورت يك شيء TableData مي‌باشد.


getResult (limit)

عبارت SQL را برمي‌گرداند.


getSQL ()

عبارت آماده شده (كه يك نمونه java.sgl.PeparedStatement است) را اگر پرس و جو آماده شده باشد برمي‌گرداند.


getPreparedStatement ()

مجموعه نتيجه را در يك نمونه از Java.sql.Resultset را هنگامي كه پرس و جو اجرا شود، برمي‌گرداند.


getReultSet

هنگامي كه پرس و جو اجرا گردد يك نمونه از Java.sql.ResultsetMetaData را برمي‌گرداند.


getReultSetMetaData ()

getcolumnAttributes()، TableData را برمي‌گرداند كه يك ساختار داده‌ دو بعدي (2D ) است. مثال زير از اين متد براي تشريح جدول استفاده مي‌كند:




function tableDesc tableName, dbcon {
if dbcon == null { dbcon = $$con; }
executeQuery qry use dbcon:
SELECT * FROM (* tableName *) WHERE 0 > 1
;
println [[*
----------------------------------------------------------------------
Name Type Display Precision Scale Nullable Class
Name Size Name
----------------------------------------------------------------------
*]];

printTable qry.getColumnAttributes()
for column('name') :<16,
column('type') :<10,
column('displaySize') :>8,
column('precision') :>11,
column('scale') :>8,
column('nullable').fmtBool() :>9,
' ', column('className'), nl; // Newline
}

// Try it out
connect to dbUrl, dbUser, dbPassword;
tableDesc 'emp';
disconnect();



چهار متد آخر در جدول 2 اشياء جاوا را برمي‌گرداند كه مي‌توانند متدهاي جاوا را دستكاري كنند يا به آنها ارسال شوند. در يك فايل كه مقاديرش با كاما از هم جدا شده‌اند، مثال زير از getResultSetMetaData() براي نسخه برداري از نتايج استفاده مي‌كند:




function printResultsetAsCSV outfile, rs, sep, closeOnExit {
if outfile == null {
outfile = getSysOut();
closeOnExit = false;
}

rsmd = rs.getResultSetMetaData();
cnt = rsmd.getColumnCount();

// Print headers
for i from 1 to cnt {
if i>1 { print <outfile> sep; }
print <outfile> rsmd.getColumnName(i);
}
println <outfile>;

// Print results
while rs.next() {
for i from 1 to cnt {
if i>1 { print <outfile> SEP; }
print <outfile> rs[i];
}
println <outfile>;
}
if closeOnExit { outfile.close(); }
}

// Try it out
connect to dbUrl, dbUser, dbPass;
executeQuery qry: SELECT * FROM emp;
printResultsetAsCSV openTextFile('result.csv', 'w'), qry, ',', true;



متدهاي فراخواني مجموعه نتايج:

همانطور كه مي‌دانيد در JDBC مقادير ستون‌ها (جداول) با متدهاي get$$$() قابل دستيابي‌ هستند.

جودواسکریپت به شما اجازه دستيابي به ستون‌ها (و ويژگيها) را از طريق اسامي و شاخص‌هايشان مي‌دهد، ولي هنوز هم مي‌توانيد هر متدي را فراخواني كنيد. براي مثال يك جدول در اوراكل را كه يك ستون LONG دارد در نظر بگيريد:




CREATE TABLE error_log(
log_id INTEGER PRIMARY KEY,
note LONG,
encoding VARCHAR(30)
);



شما مي‌توانيد با استفاده از متد gry.getBytes() بايت‌ها را گرفته و آنها را به متن تبديل نماييد.




executeQuery qry:
SELECT * FROM error_log;
;
while qry.next() {
bytes = qry.getBytes('note');
println '========== ', qry.log_id, ' ==========', nl,
encode(bytes, neverEmpty(qry.encoding, 'UTF8'));
}



اگر با qry.note به ستون دستيابي داشته باشيد، نتيجه متفاوت خواهد بود با فراخواني qry.getBytes('note') دقيقا مي‌دانيم چه كاري انجام مي‌دهيم، همچنين فراخواني متدهاي نتيجه تنها راه دستيابي به خصيصه‌هاي مختص درايور JDBC غير استاندارد است.



اجراي مستقيم updateها در SQL:

Update هاي SQL آسانتر از پرس‌وجوها هستند، چون مقدار تنها بازگشتي يك تعدادupdate است. دستورات به روز درآوردن در SQL شامل UPDATE، INSERT وDELETE مي‌باشد. مثال زير نشان مي‌دهد كه چطور يك عمل به روزرساني انجام مي‌شود:




executeUpdate upd:
UPDATE SET salary = 55000 WHERE salary < 50000
;
println unit(upd.getResult(), 'person has', 'people have'), ' got raise.';



در اين كد، تابع unit() يك تابع مفيد است. اگر پارامتر اول 1 باشد، پارامتر دوم برگردانده مي‌شود و در غير اينصورت يك شكل جمعي برمي‌گرداند كه پارامتر سوم است، اگر مشخص شده باشد، پارامتر دوم بعلاوه يك s مي‌باشد.



آماده نمودن و اجرا SQL:

از ويژگي‌هاي JDBC اين است كه از دستورات SQL كه در زمان اجرا پارامتري شده‌اند، پشتيباني مي‌كند. SQL به جاي پارامترها علامت سوال (؟) را قرار مي‌دهد. دستورات SQL را به‌طور پيوسته مي‌توان چندين بار اجرا كرد (چه با پارامتر چه بدون پارامتر). در جودواسكريپت، دستورات آماده‌سازي هم براي جستجو و هم براي به روزرساني در SQL استفاده مي‌شوند. جستجوها روي executeQuery و به روزرساني‌ها روي executUpdate اجرا مي‌شوند مانند اين نمونه:


prepare qry: SELECT emp_no, salary FROM emp WHERE salary<?;
prepare upd: UPDATE SET salary=? WHERE emp_no=?;

// Give a 10% raise for those earning less than 50,000
executeQuery qry with @1:number = 50000;
while qry.next() {
executeUpdate upd with @1:number = qry.salary * 1.10,
@2:int = qry.emp_no;
}
commit();

// Assuming auto-commit turned off.



نحوي كه براي اتصال به يك پارامتر به كار رفته شرط with @n: type مي‌باشد كه در آن n ايندكس پارامتر متصل كننده است و با 1 آغاز مي‌شود و type مي‌تواند يكي از انواع boolean، byte، date، double، float،int ، long، number، ref، bit،longvarchar، other، Java-Object، oracle-rowid، oracle-cursor and oracle-bfile باشد. به‌طور پيش‌فرض اگر نوع آن را مشخص نكنيم، String در نظر گرفته مي‌شود.



اجراي اسكريپت‌هاي پايگاه داده as-is:

قبلا ديديم كه executeSQL{…} مي‌تواند چندين دستور SQL را اجرا كند. دستورات داخل بلوك با (;) جدا مي‌شوند. تمام متن‌ها (دستورات) به سرور پايگاه داده as-is فرستاده مي‌شوند. مثال زير يك روال ذخيره شده اوراكل را ايجاد مي‌كند كه ما در بخش بعد استفاده مي‌كنيم:




executeAny [[*
CREATE PROCEDURE test_proc(
param_io IN OUT NUMBER,
param_i IN VARCHAR,
param_o OUT VARCHAR)
AS BEGIN
param_o := param_i;
IF param_io IS NOT NULL THEN
param_io := param_io + 1;
ELSE
param_io := -1000;
END IF;
END;
*]];



نحو [ [* *] ] براي نقل كردن قطعه‌اي از متن كه ممكن است شامل خطهاي جديدي باشد، به كار مي‌رود.



فراخواني روال‌هاي ذخيره شده

RDBMSهاي اصلي روال‌هاي ذخيره شده را پشتيباني مي‌كنند. JDBC يك نحو استاندارد را براي فراخواني اين روال‌ها تعريف مي‌كند. مثل اين:




{ ? = call foo(?,?,?) }



پارامترها مي‌توانندOUT ، IN و IN OUT باشند. روال مثال قبلي يعني executeAny را در اينجا فراخواني مي‌كنيم. با اين عمل يك مقدار در پارامتر param-io برگشت داده مي‌شود و يك مقدار از param-i به param-o فرستاده مي‌شود، كد زير را ببينيد:




prepareCall: { call test_proc(?,?,?) };

x = null;
y = 'abcd';

executeSQL with @1:int <=> x,
@2:varchar = y,
@3:varchar => z;

// z will be the same as y

println 'x = ', x; // Prints: x = -1000
println 'z = ', z; // Prints: z = abcd

Amir_P30
05-12-2005, 15:34
ممنون بابك جان

مطلب آموزنده وبه درد بخوري بود .

مرسي

Babak_King
06-12-2005, 18:55
قابلي نداشت عزيز