لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 11/05/22 في كل الموقع
-
مرحبا لدي كلاس اب Center class Center(models.Model): user = models.OneToOneField(User , verbose_name=_("user"), on_delete=models.CASCADE) name =models.CharField(_("Name:"),max_length=50) subtitle =models.CharField(_("Who_II:"),max_length=50) address =models.CharField(_("Address:"),max_length=50) address_detial=models.CharField(_("address_detial:"),max_length=50) number_phone =models.CharField(_("number_phone:"),max_length=50,unique=False) who_I =models.TextField(_("Who I"),max_length=250, unique=True) احتاج الى جعل جميع الكلاسات ان ترث منه مثلا جعل كلاس clinic يرث من Center class Clinic(models.Model): name_doctor =models.CharField(_("Name_Doctor:"),max_length=50) working_hours =models.CharField(_("working_hours:"),max_length=50,unique=False) waiting_time =models.IntegerField(_("waiting_time"), unique=True) price =models.IntegerField(_("Price is"), unique=True) slug =models.SlugField(_("slug") )2 نقاط
-
السلام عليكم كنت عايز حد يساعدني في هذه الخطوة انا وصلت لحد هنا في الكورس لاكن عندي ظروف وهضطر اني اقلل وقت البرمجة يعني هيبقي معاية مثلا 1:30 ،2:0:0 فاراجع فيهم ولا اطبق علي الي اتعلمتة حتي تنتهي هذه الظروف علي العلم اني هفضل في حدود شهرين علي هذا الوضع انا اسف علي الاطالة. وشكرا2 نقاط
-
مرحباً ، أعتقد أنه يوجد خطأ في المنصة. في أي درس عند عمل إعجاب لأي سؤال أو جواب تقوم الصفحة بتحديث نفسها تلقائياً والانتقال لرأس الصفحة (يحدث هذا أول مرة فقط) وبعدها يعمل بشكل صحيح دون تحديث.1 نقطة
-
1 نقطة
-
1. تثبيت تطبيق DJANGO-PAYPAL pip install django-paypal 2. أدخل التطبيق في إعدادات DJANGO الخاصة بك INSTALLED_APPS = ( ... 'paypal.standard.ipn', ) 3. أدخل نموذج PayPal عند العرض from django.views.generic import FormView from django.urls import reverse from paypal.standard.forms import PayPalPaymentsForm class PaypalFormView(FormView): template_name = 'paypal_form.html' form_class = PayPalPaymentsForm def get_initial(self): return { "business": 'your-paypal-business-address@example.com', "amount": 20, "currency_code": "EUR", "item_name": 'Example item, "invoice": 1234, "notify_url": self.request.build_absolute_uri(reverse('paypal-ipn')), "return_url": self.request.build_absolute_uri(reverse('paypal-return')), "cancel_return": self.request.build_absolute_uri(reverse('paypal-cancel')), "lc": 'EN', "no_shipping": '1', } هذا FormView عادي والقالب paypal_form.html هو نموذج Django قياسي مثل هذا: <html> <body> {{ form.render }} </body> </html> 4. تقديم عنوان URL لـ PAYPAL IPN path('paypal/', include('paypal.standard.ipn.urls')), 5. إنشاء صفحات للنجاح وفشل الدفع باي بال #views.py from django.views.generic import TemplateView class PaypalReturnView(TemplateView): template_name = 'paypal_success.html' class PaypalCancelView(TemplateView): template_name = 'paypal_cancel.html' # URL.py from . import views urlpatterns = [ path('/paypal-return/', views.PaypalReturnView.as_view(), name='paypal-return'), path('/paypal-cancel/', views.PaypalCancelView.as_view(), name='paypal-cancel'), ... ] 6. قم بإعداد الدالة التالية لاستلام مدفوعات PAYPAL الناجحة from paypal.standard.models import ST_PP_COMPLETED from paypal.standard.ipn.signals import valid_ipn_received @receiver(valid_ipn_received) def paypal_payment_received(sender, **kwargs): ipn_obj = sender if ipn_obj.payment_status == ST_PP_COMPLETED: # تحذير ! # تأكد من أن البريد الإلكتروني للمستلم هو نفسه الذي قمنا بتعيينه مسبقًا في حقل "الأعمال". (يمكن للمستخدم العبث بـ # تلك الحقول في نموذج الدفع قبل أن ينتقل إلى PayPal) if ipn_obj.receiver_email != 'your-paypal-business-address@example.com': # دفع غير صالح return # أيضًا: لنفس السبب ، تحتاج إلى التحقق من المبلغ # تلقى ، "العرف" وما إلى ذلك هي كل ما تتوقعه أو ماذا # مسموح به. try: my_pk = ipn_obj.invoice mytransaction = MyTransaction.objects.get(pk=my_pk) assert ipn_obj.mc_gross == mytransaction.amount and ipn_obj.mc_currency == 'EUR' except Exception: logger.exception('Paypal ipn_obj data not valid!') else: mytransaction.paid = True mytransaction.save() else: logger.debug('Paypal payment status not completed: %s' % ipn_obj.payment_status)1 نقطة
-
في دجانغو هناك عدة أنواع من الوراثة، حيث أن ال model يختلف عن الصف العادي بأنه يمثل جدول في قاعدة البيانات، بالتالي هناك عدة خيارات لدينا. الوراثة المجردة في حال أردنا كتابة صف مشترك و لكن لا نريد إنشاء جدول لذلك الصف، أي نقوم بكتابته لوضع صفات مشتركة و إضافتها إلى صفوف الأبناء (و بالتالي إلى الجدول الخاص بالابن في حال كان لديه واحد). أي مثلاً في حالتك قد لا تريدين وجود جدول خاص للصف Center فيمكنك إضافة ما يلي إليه: class Center(models.Model): # الكود الخاص بك # كيفية تحقيق ما سبق و شرحته class Meta: abstract = True الوراثة متعددة الجداول في حال كان يجب للصف الأب أن يكون له جدول خاص به فيمكننا كتابة وراثة عادية للصفوف في بايثون و هذا سيجعل دجانغو ينشئ جدول لكل صف. وراثة الوسيط النمط الثالث يدعى الوسيط proxy حيث يمكن إنشاء صف ابن يرث صف أب، بحيث يكون للصف الأب جدول بينما لا يكون للصف الابن جدول، و هنا دور الابن مجرد إضافة طرائق معينة للتعامل مع الجدول الذي يملكه الصف الأب. أي الابن مجرد وسيط يمكن عن طريق تنفيذ تعليمات تنفذ على الجدول الخاص بالصف الأب. نقوم عادة باستعمال هذا النوع من الوراثة في حال كنا مهتمين بإضافة طرائق خاصة للتعامل مع البيانات إلى صف ما، بحيث تكون هذه الطرق معرفة فقط في الصف الابن ( أي لا نريدها أن تكون متوفرة لمن يتعامل مع الصف الأب مباشرة). مثال على ذلك: from django.db import models class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) class MyPerson(Person): class Meta: proxy = True def do_something(self): # ... pass هنا من يتعامل مع الصف Person مباشرة لن يستطيع استعمال الدالة do_something بينما من يتعامل مع الصف MyPerson يستطيع، و كلاهما يتعاملان مع نفس الجدول، و الذي هو جدول مرتبط بالصف الأب Person. في هذا النمط يمكن فقط إضافة توابع للتعامل مع البيانات في الجدول الأب و ليس من المسموح إضافة صفات إضافية، حيث أن ذلك ممكن فقط في حال كان للابن جدول خاص به، و هنا الأمر ليس كذلك.1 نقطة
-
مرحبا زينة، الوراثة ( Inheritance ) في جانغو أو في بايثون هي تضمين محتوى كلاس في كلاس آخر. في بايثون, الصنف يمكنه أن يرث من صنف آخر حتى يحصل على الدوال و المتغيرات الموجودة فيه. لجعل الصنف يرث من صنف آخر, نضع بعد إسم الصنف قوسين و بداخلهما إسم الصنف الذي نريده أن يرث منه. في حال كان الصنف يرث من أكثر من صنف, يجب وضع فاصلة بين كل صنفين نضعهما بين القوسين. مثال على ذلك: # print_msg و دالة إسمها x يحتوي على متغير إسمه A هنا قمنا بتعريف صنف إسمه class A: x = 10 def print_msg(self): print('Hello from class A') # y و يحتوي على متغير إسمه A يرث من الصنف B هنا قمنا بتعريف صنف إسمه class B(A): y = 20 يعني حتى نجعل الصنف Clinic يرث من الصنف Center نقوم ب: class Clinic(Center): name_doctor =models.CharField(_("Name_Doctor:"),max_length=50) working_hours =models.CharField(_("working_hours:"),max_length=50,unique=False) waiting_time =models.IntegerField(_("waiting_time"), unique=True) price =models.IntegerField(_("Price is"), unique=True) slug =models.SlugField(_("slug") ) للإستزادة أكثر حول الوراثة في بايثون يمكنك قراءة مقالة: أو زيارة موسوعة حسوب. تحياتي،1 نقطة
-
import random num1 = random.randint(1, 12) num2 = random.randint(1, 12) while True: print('Choose 1 for Divison') print("Choose 2 for Subtract") mode = eval(input('enter number :')) if (mode == 1): if num1 < num2: num1, num2 = num2, num1 for i in range(5): num1 = random.randint(1, 12) num2 = random.randint(1, 12) x = eval(input(" what is " + str(num1) + " / " + str(num2) + " : ")) if x == (num1 // num2): print(" Right, the result of the question " + str(num1) + " / " + str(num2) + " = ", x) elif x != (num1 // num2): print(" You are wrong Because " + str(num1) + "/" + str(num2) + " = ", (num1 // num2)) Again = input("If you want to stop write stop Or if you want to continue, type yes : ") if (Again == "stop"): break elif (mode == 2): for i in range(5): num1 = random.randint(1, 12) num2 = random.randint(1, 12) if num1 < num2: num1, num2 = num2, num1 y = eval(input(" what is " + str(num1) + " - " + str(num2) + ":")) if y == (num1 - num2): print(" Right, the result of the question " + str(num1) + " - " + str(num2) + " = ", y) elif y != (num1 - num2): print(" You are wrong Because " + str(num1) + " - " + str(num2) + " = ", (num1 - num2)) Again = input("If you want to stop write stop Or if you want to continue, type yes : ") if (Again == "stop"): break else: print('You have entered an invalid value!') break سويت كذا مره اشتغل معي الكود بعدها صار يطلعلي خطا هل فيه مشكلة في الكود ولا صحيح1 نقطة
-
1 نقطة
-
بشكل عام يمكنك بالطبع كتابة كود بواسطة html فقط، مثلاً فورم معين، أو موقع شخصي. و لكن سيبقى جامداً و لا أحد سيحب التعامل معه. في حال كنت تتعلم html فقط، من الجيد كتابة هكذا كود، أي قم مثلاً بكتابة فورم معين، أو قم بإنشاء موقع يحوي على معلوماتك و خبراتك، و لكن بعدها يجب عليك الانتقال لتعلم ال css و ذلك لتضفي بعض الجمالية على الموقع، من السهل تعلم القليل من css و عندها يمكنك صنع موقع شخصي جيد جداً، و تجريب الكثير من التنسيقات. رغم كل هذا سيبقى عليك تعلم javascript لإضافة بعض الديناميكية على موقعك، حيث أن استعمال css, html فقط يجعل الموقع ستاتيكي غير قابل للتعديل عليه من قبل المستخدم أو التفاعل معه بطريقة جيدة.1 نقطة
-
هل تقوم بإيقاف الفيديو قبل النزول و الإعجاب بتعليق ما؟ إن الفيديو الخاص بالدرس يأخذ بعض الوقت ليتم تحميله و ذلك حسب سرعة النت لديك، و عندما يتم تحميله ينتقل التركيز focus في الصفحة إليه. قم بالإنتظار حتى يتم تحميل القالب الخاص بالفيديو و اضغط على إيقاف و ذلك سيحل المشكلة التي تحصل معك.1 نقطة
-
ما الخطأ في الكود ، اريده ان يطبع العناصر على شكل قائمة، من اجل ان يطبع القائمة وبعدها يعكس العناصر، علما انه يطبع فقط العنصر الاخير في القائمة، سوف ارفق لكم نص السؤال وهو :Write a program that starts off with an empty list, then prompts the user to enter the number of elements he wants to add to the list, then let the user add the elements to the list, your program should print the list then print it reversed This is how your output should be : Enter the number of elements in the list : 4 Enter the elements : January February March April Your list is [January , February, March, April ] Your reversed list is [April, March, February, January] note: {underlined words means user input}1 نقطة
-
1 نقطة
-
السلام عليكم كنت قد قرأت عن انه بعد الحصول علي الشهاده ستقوم اكاديميه حسوب بمساعدتي للحصول علي اول عمل حر و قد اقتربت من انهاء الدوره ف اريد ان اعرف معلومات اكثر عن هذا1 نقطة
-
أظن في مثل هذه الظروف من الأفضل أن تبقى على القليل المستمر، أي لا تقم بإيقاف التعلم وبنفس الوقت لا تقم بإيقاف المراجعة. يمكنك تحقيق ذلك من خلال تقسيم الوقت بشكل جيد، فمثلا الساعتان المتوفرتان لديك في اليوم هم في الأسبوع حوالي 14 ساعة، أي وقت لا يمكن الاستهانة به أليس كذلك! فاجعل مثلاً 7 ساعات منهم تعلم جديد و7 ساعات للمراجعة، أو أي تقسيم أنت سترتاح به وستحقّق تقدّماً به. لكن لا أنصحك بتاتاً أن تقوم بتجميد التعلّم خاصة أن فترة شهرين طويلة نسبيّاً، فالأفضل أن تقوم بهذا التقسيم وتحقق تعلّماً جديداً وتثبيتاً لما قد قمت بتعلّمه. أريد أن أضيف أمر آخر أيضاً، هو أن التطبيق بمثابة المراجعة. أي أنك عندما تطبّق أمراً ما فكأنك قمت بمراجعته وفهم ماذا يفعل وكيف يستخدم أي أن المراجعة قريبة جدّاً من التطبيق، لكن التطبيق هو المستوى الأفضل بالطبع، فقم بتطبيق ما تعلّمته في وقت المراجعة، وتعلّم أمور جديدة في الوقت المتبقي بالتوفيق إن شاء الله1 نقطة
-
الخطأ أنك تقوم بتخزين العنصر الأول مثلاً april في المتحول el ثم تقوم بتخزين العنصر الثاني march بنفس العنصر el ومنه يكتب فوق القيمة القديمة وتذهب الأولى. يجب عليك أن تستخدم مصفوفة لحفظ العناصر لتتجنب هذه المشكلة: print("[]:") num=int(input("...")) y=0 list = [] # عرفنا المصفوفة while y<num: y+=1 el=input("Enter Element:") list.append(el) #أضفنا هنا العنصر المدخل داخل المصفوفة الآن أصبح لديك جميع العناصر داخل المصفوفة list كل ما عليك فعله طباعتها معكوسة! بالتوفيق1 نقطة
-
على الإهتمام كثيراً بمهارتك والخبرات التي لدي التي تستطيع تقديمها لأصحاب المشاريع بحيث تعتبر المهارات والخبرات هي المنتج التي تقدمها إلى العملاء وحصول على فرصة عمل . يمكنك التركيز على التالي من أجل جلب أول عميل لك :- كتابة نبذة توضح مهاراتك والخبرات والخدمات التي تستطيع تقديمها للعملاء الاهتمام بمعرض الأعمال وعرض الأعمال ذات الجودة العالية التي قمت بها سابقًا عند حصولك على عمل اهتم بتقديم أعلى جودة للعمل المنفذ لتحصل على تقييم جيد ضمن حسابك قدم عروض تبرز فيها ما الذي تستطيع تقديمه لصاحب العمل ومدى خبرتك الفعلية في مجال المشروع يمكنك الإطلاع على هذه المقالات على مدونة مستقل وهي مفيدة من أجل الحصول على العميل الأول لماذا يتجاهل أصحاب المشاريع عروضك على مستقل؟ للمستقلين الجدد: كيف تبني معرض أعمالك باحترافية؟ هذه أيضاً بعض المقالات على الأكاديمة تفيدك في الحصول على العميل الأول1 نقطة
-
اذا تم رفض قبول مدونه بلوجر بادسنس ماهي افضل شركه اعلانات بديله للادسنس ??1 نقطة
-
لديك كل من هذه الشركات : Media.net real content network PropellerAds PopCash PopAds RevContent Adsterra MadAds Media Adrecover1 نقطة
-
لا تُعدّ الشبكات -بقدر ما يَهُمّ البرامج- أكثر من مجرد مصدرٍ يُمكِن قراءة البيانات منه، أو مقصدٍ يُمكِن إرسال البيانات إليه. يُسطِّح ذلك الأمور إلى درجةٍ كبيرة؛ فليس التعامل مع الشبكات بالتأكيد بنفس سهولة التعامل مع الملفات مثلًا، وتُمكِّنك لغة جافا مع ذلك من إجراء الاتصالات الشبكية باستخدام مجاري تدفق streams الدْخل والخرج بنفس الطريقة التي تَستخدِمها بها للتواصل مع المُستخدِم أو لمعالجة الملفات. بالرغم من ذلك، تنطوي عملية إجراء اتصال شبكي بين حاسوبين على الكثير من التعقيدات؛ حيث ينبغي أن يتفقا بطريقةٍ ما على فتح اتصالٍ بينهما. بالإضافة إلى ذلك، إذا كان كلُ حاسوبٍ قادرًا على إرسال البيانات إلى الحاسوب الآخر، تُصبِح مزامنة التواصل بينهما مشكلةً أخرى، ولكن تُعدّ الأساسيات في العموم هي نفسها. تُعدّ java.net إحدى حزم packages جافا القياسية، حيث تتضمَّن عدَّة أصنافٍ للتعامل مع الشبكات، كما تدعم طريقتين مختلفتين لإجراء عمليات الدخل والخرج خلال الشبكة. تعتمد الطريقة الأولى عالية المستوى high level على شبكة الإنترنت العالمية World Wide Web، وتُوفِّر إمكانياتٍ للاتصال الشبكي مماثلةً لتلك الإمكانيات التي يَستخدِمها متصفح الإنترنت عند تحميله للصفحات. يُعدّ java.net.URL و java.net.URLConnection الصنفين الأساسين المُستخدَمين مع هذا النوع من الشبكات. يُمثِّل أي كائنٍ من النوع URL تمثيلًا مجرّدًا لمُحدِّد مورد مُوحَّد Universal Resource Locator؛ فقد يكون عنوانًا لمستند HTML، أو لأي موردٍ آخر؛ بينما يُمثِّل أي كائنٍ من النوع URLConnection اتصالًا شبكيًا مع إحدى تلك الموارد. من الجهة الأخرى، تَنظر الطريقة الثانية الأكثر عمومية وأهمية للشبكة بمستوى منخفض قليلًا low level؛ حيث تعتمد على فكرة المقابس socket المُستخدَمة لإنشاء اتصالٍ مع برنامجٍ آخر خلال الشبكة. يشتمل الاتصال عبر الشبكة على مقبسين، واحدٌ في كل طرف من طرفي الاتصال، وتَستخدِم جافا الصنف java.net.Socket لتمثيل المقابس المُستخدَمة بالاتصال الشبكي. أُخِذَت كلمة مِقبس من التصور المادي لتوصيل الحاسوب بسلكٍ بهدف ربطه بشبكة، ولكن ما نعنيه بالمقبس هنا أنه كائنٌ ينتمي إلى الصنف Socket. يَستطيع أي برنامجٍ أن يحتوي على مجموعةٍ من المقابس بنفس الوقت؛ بحيث يتَصِل كُلٌ منها ببرنامجٍ آخرٍ مُشغَّلٍ على حاسوبٍ آخر ضمن الشبكة، أو ربما على نفس الحاسوب. تَعتمِد جميع تلك الاتصالات على الاتصال الشبكي المادي نفسه. يتناول هذا مقال مقدمةً مُختصرة عن أساسيات أصناف الشبكات، كما يشرح علاقتها بمجاري الدْخَل والخرج. محددات الموارد والصنفان URL و URLConnection يُستخدَم الصنف URL لتمثيل الموارد resources بشبكة الويب العالمية World Wide Web، حيث يَملُك كل موردٍ منها عنوانًا يُميّزها، والذي يحتوي على معلوماتٍ كافية تُمكِّن متصفح الويب من العثور على المورد على الشبكة واسترجاعه. يُعرَف هذا العنوان باسم "محدِّد الموارد المُوحَّد universal resource locator - URL"، كما يُمكِنه في الواقع أن يشير إلى مواردٍ من مصادر أخرى غير الويب، فقد يُشير مثلًا إلى إحدى ملفات الحاسوب. يُمثِّل أي كائنٍ ينتمي إلى الصنف URL عنوانًا معينًا، وبمجرد حصولك على واحدٍ من تلك الكائنات، يُمكِنك إنشاء كائنٍ من الصنف URLConnection للاتصال مع المورد الموجود بذلك العنوان. يُكْتَب محدِّد الموارد المُوحَّد عادةً بهيئة سلسلةٍ نصية، مثل "http://math.hws.edu/eck/index.html"، ولكن هناك أيضًا محدِّدات مواردٍ نسبية relative url، والتي يُمكِنها تخصيص موقع موردٍ معين بالنسبة لموقع موردٍ آخر، والذي يَعمَل في تلك الحالة كأنه أساسٌ أو سياقٌ لمحدِّد المورد النسبي؛ فإذا كان السياق هو "http://math.hws.edu/eck/" مثلًا، فسيُشير المورد النسبي غير الكامل "index.html" في تلك الحالة إلى الآتي: "http://math.hws.edu/eck/index.html" لاحِظ أن كائنات الصنف URL ليست مجرد سلاسلٍ نصية، ولكن يُمكِن إنشاؤها من التمثيل النصي لمحدِّد موردٍ موحَّد، كما يُمكِن إنشاء تلك الكائنات بالاستعانة بكائن URL آخر يُوفِّر سياقًا معينًا مع سلسلةٍ نصيةٍ تُوفِّر مُحدِّد المورد النسبي لذلك السياق. انظر تعريف البناة constructors المُعرَّفة بالصنف: public URL(String urlName) throws MalformedURLException و public URL(URL context, String relativeName) throws MalformedURLException حيث تُبلِّغ تلك البناة عن استثناء exception من النوع MalformedURLException، إذا لم تكن السلاسل النصية المُمرَّرة إليها مُمثِلةً لمحدِّدات مواردٍ مُوحَّدةٍ سليمة. لاحِظ أن الصنف MalformedURLException هو صنفٌ فرعيٌ من الصنف IOException، أي أنه يتطلَّب معالجةً إلزاميةً للاستثناءات. بمجرد حصولنا على كائن URL سليم، يُمكِننا استدعاء تابِعه openConnection() لإجراء اتصالٍ معه؛ حيث يُعيد هذا التابع كائنًا من النوع URLConnection، والذي يُمكِننا استدعاء تابعه getInputStream() لإنشاء كائنٍ من النوع InputStream، ونَستطيع بذلك قراءة بيانات المورد الذي يُمثِّله الكائن. انظر الشيفرة التالية: URL url = new URL(urlAddressString); URLConnection connection = url.openConnection(); InputStream in = connection.getInputStream(); قد يُبلِّغ التابعان openConnection() و getInputStream() عن استثناؤات من النوع IOException. بمجرد إنشاء كائن الصنف InputStream، يُمكِننا أن نقرأ بياناته كما ناقشنا بالأقسام السابقة، بما في ذلك تضمينه داخل مجاري الدخل input stream من أنواعٍ أخرى، مثل BufferedReader أو Scanner. قد تؤدي قراءة بيانات المجرى إلى حدوث استثناءات بالتأكيد. يتضمَّن الصنف URLConnection توابع نسخ instance methods أخرى مفيدة؛ حيث يُعيد التابع getContentType() مثلًا سلسلةً نصيةً من النوع String تَصِف نوع المعلومات الموجودة بالمورد الذي يُمثِله كائن الصنف URL، كما يُمكِنه إعادة القيمة null إذا لم تكن نوعية المعلومات معروفةً بعد، أو لم يكن تحديد نوعها ممكنًا؛ أي قد لا نتمكَّن من معرفة نوع المعلومات حتى نُنشِئ مجرى المْدْخَلات باستدعاء التابع getInputStream() ثم التابع getContentType()، حيث يُعيد هذا التابع سلسلةً نصيةً بصيغةٍ تُعرَف باسم نوع الوسيط "mime type"، مثل "text/plain" و "text/html" و "image/jpeg" و "image/png" وغيرها. تتكوَّن جميع أنواع الوسائط من جزئين: نوعٌ عام، مثل "text" أو "image"، ونوعٌ أكثر تحديدًا من النوع العام، مثل "html" أو "png"؛ فإذا كنت مهتمًا بالبيانات النصية فقط مثلًا، يُمكِنك فحص فيما إذا بدأت السلسلة النصية المُعادة من التابع getContentType() بكلمة "text". كان الهدف الأساسي من أنواع الوسائط هو مجرد وصف محتويات رسائل البريد الإلكتروني؛ فالاسم "mime" هو أساسًا اختصارٌ لعبارة "Multipurpose Internet Mail Extensions"، ولكنها مُستخدَمةٌ الآن على نطاقٍ واسع لتحديد نوع المعلومات الموجودة بملفٍ أو بموردٍ آخر عمومًا. لنناقش الآن مثالًا قصيرًا على استخدام كائنٍ من النوع URL لقراءة البيانات الموجودة بمُحدِّد موردٍ معين، حيث يُنشِئ البرنامج الفرعي التالي اتصالًا مع المورد الذي يُمثِّله الكائن، ثم يَتأكَّد من أن البيانات التي يُشير إليها مُحدِّد المورد من النوع النصي، ويَنسَخ بعد ذلك النص إلى الشاشة. قد تُبلِّغ الكثير من العمليات المُضمَّنة بالبرنامج عن استثناءات، ولذلك أضفنا عبارة "throws IOException" أثناء التصريح عن البرنامج الفرعي؛ لنترك القرار للبرنامج المُستدعِي باختيار ما ينبغي فعله عند حدوث خطأٍ معين: static void readTextFromURL( String urlString ) throws IOException { // 1 URL url = new URL(urlString); URLConnection connection = url.openConnection(); InputStream urlData = connection.getInputStream(); // 2 String contentType = connection.getContentType(); System.out.println("Stream opened with content type: " + contentType); System.out.println(); if (contentType == null || contentType.startsWith("text") == false) throw new IOException("URL does not seem to refer to a text file."); System.out.println("Fetching context from " + urlString + " ..."); System.out.println(); // 3 BufferedReader in; // للقراءة من مجرى الدخل الخاص بالاتصال in = new BufferedReader( new InputStreamReader(urlData) ); while (true) { String line = in.readLine(); if (line == null) break; System.out.println(line); } in.close(); } // end readTextFromURL() حيث: [1]: افتح اتصالًا مع مُحدِّد مورد موحَّد واحصل على مجرى دخل لقراءة البيانات منه. [2]: اِفحص فيما إذا كانت المحتويات من النوع النصي. [3]: اِنسَخ الأسطر النصية من مجرى الدخل إلى الشاشة حتى تصِل إلى نهاية الملف، أو حتى يقع خطأ. يَستخدِم البرنامج FetchURL.java البرنامج الفرعي المُعرَّف بالأعلى. بإمكانك تخصيص مُحدِّد المورد المطلوب أثناء تشغيل البرنامج من خلال سطر الأوامر؛ وإذا لم تُخصِّصه، فسيطلُب منك البرنامج تخصيصه. يَسمَح البرنامج بمُحدِّدات الموارد البادئة بكلمة "http://" أو "https://" إذا كان المُحدِّد يُشير إلى مورد بشبكة الإنترنت؛ أو تلك البادئة بكلمة "file://" إذا كان المُحدِّد يِشير إلى إحدى ملفات حاسوبك؛ أو تلك البادئة بكلمة "ftp://" إذا كان المُحدِّد يَستخدِم بروتوكول نقل الملفات File Transfer Protocol. إذا لم يبدأ بأي من تلك الكلمات، فسيُضيف كلمة "http://" تلقائيًا إلى بداية مُحدِّد المورد. يُمكِنك أن تُجرِّب مثلًا مُحدِّد المورد "math.hws.edu/javanotes" لاسترجاع الصفحة الرئيسية لهذا الكتاب من موقعه الإلكتروني، كما يُمكِنك تجريبه أيضًا مع بعض المُدْخَلات غير السليمة، لترى نوعية الأخطاء المختلفة التي قد تَحصُل عليها. بروتوكول TCP/IP والخوادم والعملاء يعتمد نقل المعلومات عبر شبكة الانترنت على بروتوكولين، هما بروتوكول التحكم بالنقل Transmission Control Protocol وبروتوكول الإنترنت Internet Protocol، ويُشار إليهما مجتمعين باسم TCP/IP. هناك أيضًا بروتوكولٌ أبسط يُسمَى UDP؛ حيث يُمكِن اِستخدَامه بدلًا من TCP بتطبيقاتٍ معينة، وهو مدعومٌ من قِبل جافا. ولكن سنكتفي هنا بمناقشة TCP/IP الذي يُوفِّر نقلًا موثوقًا ثنائي الاتجاه للمعلومات بين حواسيب الشبكات. لكي يتمكَّن برنامجان من تبادل المعلومات عبر بروتوكول TCP/IP، ينبغي أن يُنشِئ كلٌ منهما مقبسًا socket، كما ينبغي أن يكون المقبسان متصلين ببعضهما. تُنقَل المعلومات بينهما بعد إنشاء هذا الاتصال باستخدام مجاري تدفق streams دْخَل وخَرج. يَملُك كل برنامجٍ منهما مجريين للمُدخلات وللمُخرجات، ويَستطيع أحدهما مثلًا إرسال بعض البيانات إلى مجرى الخرج الخاص به، وستُنقَل إلى الحاسوب الآخر، ومنه إلى مجرى الدْخَل الخاص بالبرنامج الموجود على الطرف الآخر من الاتصال الشبكي. عندما يقرأ هذا البرنامج البيانات من مجرى الدْخَل الخاص به، يكون قد تَسلَّم بذلك البيانات التي أُرسلَت إليه عبر الشبكة. تَكْمُن الصعوبة في إنشاء الاتصال الشبكي ذاته من الأساس، حيث تنطوي العملية على مقبسين. يجب أن يُنشِئ أحد البرنامجين مقبسًا socket ويتركه ينتظر طلب اتصالٍ من المقبس الآخر، ويُقال أن المقبس المُنتظِر يَستمِع للاتصال. ويُنشِئ البرنامج الآخر من الجهة الأخرى من الاتصال المُفترَض مقبسًا آخرًا ليُرسِل طلب اتصالٍ إلى المقبس المُستمِع، والذي يَستجيب بدوره عندما يَتَسلّم طلب الاتصال، وبذلك يكون الاتصال قد اُنشِئ. بمجرد إنشاء الاتصال، يستطيع كل برنامج الحصول على مجريين للدخل والخرج لإرسال البيانات عبر الاتصال، ويستمر الاتصال بين البرنامجين من خلال تلك المجاري حتى يُقرِّر أحدهما إغلاقه. يُطلَق على البرنامج المُنشِئ لمقبس الاستماع listening socket اسم "الخادم server"؛ بينما يُطلَق على المقبس اسم "مقبس الخادم server socket". في المقابل، يُطلَق على البرنامج الذي يَتصِل بالخادم اسم "العميل client"؛ بينما يُطلَق على المقبس الذي يَستخدِمه لإجراء الاتصال اسم "مقبس العميل client socket". تكمن الفكرة ببساطة في أن الخادم يَقبُع بمكانٍ ما داخل الشبكة منتظرًا طلبات الاتصال من العملاء. يُمكِننا إذًا أن نفكر بالخادم وكأنه يُقدِم نوعًا معينًا من الخدمات، في حين يحاول العميل الوصول إلى تلك الخدمة عن طريق الاتصال بالخادم. يُعرَف ذلك باسم نموذج العميل / الخادم client/server model لنقل المعلومات عبر الشبكة. يستطيع الخادم أيضًا وبالكثير من التطبيقات أن يُوفِّر اتصالات لأكثر من عميلٍ واحدٍ بنفس الوقت؛ فعندما يَتصِّل عميلٌ معين بمقبس الاستماع الخاص بخادمٍ من هذا النوع، لا يتوقف المقبس عن الاستماع، وإنما يستمر بالاستماع إلى أي طلبات اتصالٍ إضافية بنفس الوقت الذي يَخدِّم خلاله العميل الأول. يتطلَّب ذلك استخدام الخيوط threads التي سنناقش طريقة عملها بالمقال التالي. يَستخدِم الصنف URL -الذي نُوقِش ببداية هذا المقال- مقبس عميلٍ لإجراء أي اتصالٍ ضروري عبر الشبكة؛ بينما يتواجد في الجهة الأخرى لذلك الاتصال برنامج خادم لاستقبال طلب الاتصال من كائن الصنف URL، ويقرأ طلبه بخصوص إحدى الملفات الموجودة بحاسوب الخادم، ثم يَستجيب للطلب بإرسال محتويات الملف المطلوب إلى ذلك الكائن عبر الشبكة. وأخيرًا، يُغلِق الخادم الاتصال بعد انتهائه من نقل البيانات. يجب أن يَجِد برنامج العميل طريقةً ما لتخصيص أي حاسوبٍ من ضمن كل تلك الحواسيب الموجودة بالشبكة يريد الاتصال به. في الحقيقة، يَملُك كل حاسوبٍ بشبكة الإنترنت عنوان IP يُميّزه عن غيره؛ كما يُمكِن الإشارة إلى كثيرٍ من الحواسيب باستخدام أسماء المجال domain names، مثل "math.hws.edu" أو "http://www.whitehouse.gov" (انظر مقال "الإنترنت وما بعده وعلاقته بجافا"). تتكوَّن عناوين IP (أو IPv4) التقليدية من أعدادٍ صحيحةٍ من "32 بت"، وتُكْتَب عادةً بصيغةٍ عشريةٍ مُنقطَّة، مثل "64.89.144.237"؛ بحيث يُمثِّل كل عددٍ من الأعداد الأربعة ضمن ذلك العنوان عددًا صحيحًا من "8 بتات" بنطاقٍ يتراوح من "0" إلى "255". يتوفَّر الآن إصدارٌ أحدث من بروتوكول الإنترنت هو IPv6؛ حيث تتكوَّن عناوينه من أعدادٍ صحيحة من "128 بت"، وتُكْتَب عادةً بصيغةٍ ست عشريّة hexadecimal، ويَستخدِم نقطتين وربما بعض المعلومات الإضافية الآخرى. ما يزال الإصدار الأحدث IPv6 نادرًا من الناحية العملية. يُمكِن لأي حاسوب أن يمتلك مجموعةً من عناوين IP، كما قد يمتلك عناوين IPv4 و IPv6، والتي يُعرَف إحداها عادةً باسم "عنوان الاسترجاع loopback"؛ حيث تستخدِم البرامج عنوان الاسترجاع، إذا كانت تريد التواصل مع برامجٍ أخرى على نفس الحاسوب. يَملُك عنوان الاسترجاع عنوان IPv4 هو "127.0.0.1"، ويُمكِننا الإشارة إليه باستخدام اسم المجال "localhost". بالإضافة إلى ذلك، قد يكون هناك عناوين IP أخرى مرتبطة باتصالٍ شبكي مادي، كما يحتوي عادةً أي حاسوب على أداةٍ لعرض عناوين IP الموجودة به. كتب المؤلف البرنامج ShowMyNetwork.java ليَفعَل الشيء نفسه، وحَصَل على الخرج التالي بعد أن شغَّله على حاسوبه: en1 : /192.168.1.47 /fe80:0:0:0:211:24ff:fe9c:5271%5 lo0 : /127.0.0.1 /fe80:0:0:0:0:0:0:1%1 /0:0:0:0:0:0:0:1%0 تُشيِر أول كلمة بكل سطرٍ منهما إلى اسم بطاقة الشبكة network interface، والتي يَفهَم معناها نظام التشغيل فقط، كما يحتوي كل سطرٍ على عناوين IP الخاصة بالبطاقة؛ حيث تُشير بطاقة "lo0" على سبيل المثال إلى عنوان الاسترجاع loopback الذي يَملُك عادةً عنوان IPv4 هو "127.0.0.1". في الحقيقة، إن العدد "192.168.1.47" هو الأكثر أهميةً من بين كل تلك الأعداد؛ فهو يُمثِّل عنوان IPv4 المُستخدَم للاتصالات عبر الشبكة؛ أما الأعداد الآخرى فهي عناوين IPv6. ملاحظة: لا تُعدّ الخطوط المائلة ببداية كل عنوان جزءًا فعليًا منه. قد يحتوي الحاسوب على عدّة برامجٍ تُجرِي اتصالات شبكية بنفس الوقت، أو على برنامجٍ واحد يتبادل المعلومات مع مجموعةٍ من الحواسيب الأخرى؛ حيث يملك كلُّ اتصالٍ شبكي رقم مَنفَذ port number إلى جانب عنوان IP. يتكوَّن رقم المَنفَذ ببساطة من عددٍ صحيحٍ موجبٍ من "16 بت". لا يَستمِع الخادم إلى الاتصالات في العموم، وإنما يَستمِع إلى الاتصالات الواقعة برقم منفذٍ معين، ولذلك يجب أن يَعرِف أي عميلٍ مُحتمَل لخادمٍ معين كُلًا من عنوان الإنترنت (أو اسم المجال) الخاص بالحاسوب الذي يَعمَل عليه ذلك الخادم، وكذلك رقم المَنفَذ الذي يَستمِع إليه الخادم. تَستمِع خوادم الإنترنت عمومًا إلى الاتصالات الواقعة برقم المنفذ "80"، كما تحدث بعض خدمات الويب القياسية الأخرى بأرقام منافذٍ قياسيةٍ أخرى. تُعدّ أرقام المنافذ الأقل من "1024" ضمن أرقام المنافذ القياسية، وهي في الواقع محجوزةٌ لخدماتٍ معينة، ولذلك إذا أنشأت خادمك الخاص، ينبغي أن تَستخدِم رقم منفذٍ أكبر من "1024". المقابس والصنف Socket تُوفِّر حزمة java.net الصنفين ServerSocket و Socket لتنفيذ اتصالات بروتوكول TCP/IP؛ حيث يُمثِل كائنٌ من الصنف ServerSocket مقبس استماع listening socket ينتظر طلبات الاتصال من العملاء؛ بينما يُمثِّل كائنٌ من الصنف Socket طرفًا واحدًا من اتصالٍ فعلي، حيث يمكن أن يُمثِّل عميلًا قد أرسل طلبًا إلى خادم. يستطيع الخادم إنشاء كائنٍ من هذا الصنف -أي Socket-، ويطلب منه معالجة طلب اتصالٍ من عميلٍ معين، ويَسمَح ذلك للخادم بإنشاء عدة كائناتٍ من ذلك الصنف لمعالجة اتصالات عديدة. في المقابل، لا تُشارِك كائنات الصنف ServerSocket بالاتصالات، فهي فقط تستمع إلى طلبات الاتصال، وُتنشِئ كائناتٍ من الصنف Socket لمعالجة الاتصالات الفعلية. عندما تُنشِئ كائنًا من الصنف ServerSocket، عليك أن تُخصِّص رقم المَنفَذ port number الذي سيستمع إليه الخادم. انظر الباني constructor الخاص بهذا الصنف: public ServerSocket(int port) throws IOException يجب أن يقع رقم المَنفَذ ضمن نطاقٍ يتراوح من "0" إلى "65535"، كما يجب أن يكون أكبر من "1024". يُبلِّغ الباني عن استثناء من النوع SecurityException، إذا كان رقم المَنفَذ المُخصَّص أقل من "1024"؛ كما يُبلِّغ عن استثناء من النوع IOException، إذا كان رقم المَنفَذ المُحدَّد مُستخدَمًا بالفعل. يُمكِنك مع ذلك تمرير القيمة "0" مثل معاملٍ للتابع لتخبره بأن الخادم بإمكانه الاستماع إلى أي رقم مَنفَذٍ متاح. بمجرّد إنشاء كائنٍ من الصنف ServerSocket، فسيبدأ بالاستماع إلى طلبات الاتصال من العملاء. يَستقبِل التابع accept() المُعرَّف بالصنف ServerSocket طلبًا، ثم يُنشِئ اتصالًا مع العميل، ويعيد كائنًا من النوع Socket يُمكِن اِستخدَامه للاتصال مع العميل. يُعرَّف التابع accept() على النحو التالي: public Socket accept() throws IOException عندما تَستدعِي التابع accept()، فإنه لا يعيد قيمته حتى يتسلَّم طلب اتصال، أو حتى يقع خطأ ما، ولذلك يُعدّ تابعًا مُعطِّلًا block أثناء انتظاره للطلب؛ لأن البرنامج -أو بتعبير أدق الخيط thread الذي اِستدعَى التابع- لا يستطيع فعل ذلك بأي شيءٍ آخر، بينما تستطيع الخيوط الآخرى أن تُكمِل عملها بصورةٍ طبيعية. يُمكِنك استدعاء accept() مرةً بعد أخرى لتَستقبِل عدة طلبات اتصال، وسيستمر كائن الصنف ServerSocket بالاستماع إلى طلبات الاتصال إلى أن يُغلَق باستدعاء تابعه close()، أو إلى أن يَحدُث خطأ، أو أن ينتهي البرنامج بطريقةٍ ما. لنفترض أننا نريد إنشاء خادمٍ يَستمِع إلى رقم المَنفَذ "1728"، ويستمر باستقبال طلبات الاتصال طوال فترة تشغيل البرنامج. إذا كان provideService(Socket) تابعًا مسؤولًا عن معالجة اتصالٍ مع عميلٍ واحد، يُمكِننا أن نَكْتُب برنامج الخادم التالي: try { ServerSocket server = new ServerSocket(1728); while (true) { Socket connection = server.accept(); provideService(connection); } } catch (IOException e) { System.out.println("Server shut down with error: " + e); } من جهة العميل، يُمكِننا إنشاء كائنٍ من النوع Socket باستخدام أحد البُناة constructors المُعرَّفة بالصنف Socket. يُمكِننا استخدام الباني التالي لنتمكَّن من الاتصال بخادمٍ معين نَعرِف الحاسوب الذي يَعمَل عليه وكذلك رقم المَنفَذ الذي يَستمِع إليه: public Socket(String computer, int port) throws IOException بإمكاننا تمرير اسم المجال domain name أو عنوان IP على أنه قيمةٌ للمعامل الأول بالباني السابق. سيُعطِّل block هذا الباني التنفيذ حتى يُنشَئ الاتصال أو حتى يَحدُث خطأً. إذا كان لدينا مقبسٌ مُتصِلٌ بغض النظر عن طريقة إنشائه، يُمكِننا استدعاء أيٍّ من توابع الصنف Socket، مثل التابعين getInputStream() و getOutputStream() الذين يعيدان كائناتٍ من النوع InputStream و OutputStream على الترتيب، وبذلك، نكون قد حَصلنا على مجاري تدفق بإمكاننا اِستخدَامها لنقل المعلومات عبر هذا الاتصال. تُوضِح الشيفرة التالية الخطوط العريضة لتابع يُجرِي اتصالًا من طرف العميل: // 1 void doClientConnection(String computerName, int serverPort) { Socket connection; InputStream in; OutputStream out; try { connection = new Socket(computerName,serverPort); in = connection.getInputStream(); out = connection.getOutputStream(); } catch (IOException e) { System.out.println( "Attempt to create connection failed with error: " + e); return; } . . // استخدم المجريين in و out لتبادل المعلومات مع الخادم . try { connection.close(); // قد تعتمد على الخادم لغلق الاتصال بدلًا من غلقه بنفسك } catch (IOException e) { } } // end doClientConnection() [1] افتح اتصالًا مع الحاسوب ورقم المَنفَذ المُخصَّصين للخادم، ثم انقل المعلومات عبر الاتصال. تُسهِّل كل تلك الأصناف من التعقيدات الكثيرة التي ينطوي عليها نقل المعلومات عبر الشبكات؛ أما إذا كنت تَجِد هذا صعبًا بالفعل، فإنه إذًا أكثر صعوبة. إذا كانت الشبكات جديرةً بالثقة كليًا، فلربما كان الأمر بنفس السهولة التي وُصِفت بها هنا، ولكنها ليست كذلك؛ ولهذا ينبغي كتابة برامج الشبكات بصورةٍ متينة robust تُمكِّنها من التعامل مع أخطاء الشبكات والبشر، ولكننا لن نناقش هذا هنا. لقد شرحنا الأفكار الأساسية لبرمجة الشبكات عمومًا، وسنكتفي بما تعرَّضنا له من تطبيقاتٍ بسيطة. لنفحص الآن أمثلةً قليلة على برمجة الخادم / العميل. برنامج عميل/خادم بسيط يتكوَّن المثال الأول من برنامجين تتوفَّر شيفرتهما بالملفين DateClient.java و DateServer.java؛ حيث يُمثِّل الأول عميلًا شبكيًا network client بسيطًا؛ بينما يُمثِّل الآخر خادمًا server. يُنشِئ العميل اتصالًا مع الخادم، ويقرأ سطرًا نصيًا واحدًا منه، ثم يَعرِضه على الشاشة؛ حيث يتكوَّن هذا السطر من التاريخ والتوقيت الحالي بالحاسوب الذي يَعمَل عليه الخادم. يجب بالطبع أن يَعرِف العميل أي حاسوبٍ يَعمَل عليه الخادم، وكذلك رقم المَنفَذ port الذي يَستمِع إليه الخادم حتى يتمكَّن من إنشاء اتصالٍ معه. يُمكِن أن يقع رقم المنفذ بين "1025" و "65535" عمومًا (الأرقام الواقعة بين "1" و "1024" محجوزةٌ للخدمات القياسية، ولا ينبغي استخدامها للخوادم الأخرى)، ولا يُحدِِث ذلك أي فرقٍ بشرط أن يَستخدِم الخادم والعميل نفس رقم المنفذ، ولنفترض أن الخادم بهذا المثال يَستمِع إلى رقم المنفذ "32007". يُمكِنك تمرير اسم أو عنوان IP الخاص بحاسوب الخادم مثل وسيط سطر أوامر للبرنامج DateClient أثناء تشغيله؛ فإذا كان الخادم مثلًا مُشغَّلًا على حاسوبٍ اسمه "math.hws.edu"، يُمكِنك أن تُشغِّل العميل باستخدام الأمر java DateClient math.hws.edu. في حالة عدم تخصيص حاسوب الخادم على أنه وسيط بسطر الأوامر، سيطلب البرنامج منك أن تُدْخِله. انظر الشيفرة الكاملة لبرنامج العميل: import java.net.*; import java.util.Scanner; import java.io.*; // 1 public class DateClient { public static final int LISTENING_PORT = 32007; public static void main(String[] args) { String hostName; // اسم حاسوب الخادم Socket connection; // رقم منفذ الاتصال مع الخادم BufferedReader incoming; // لقراءة البيانات من الاتصال // اقرأ حاسوب الخادم من سطر الأوامر if (args.length > 0) hostName = args[0]; else { Scanner stdin = new Scanner(System.in); System.out.print("Enter computer name or IP address: "); hostName = stdin.nextLine(); } // أجرِ الاتصال ثم اقرأ سطرًا نصيًا واعرضه try { connection = new Socket( hostName, LISTENING_PORT ); incoming = new BufferedReader( new InputStreamReader(connection.getInputStream()) ); String lineFromServer = incoming.readLine(); if (lineFromServer == null) { // 2 throw new IOException("Connection was opened, " + "but server did not send any data."); } System.out.println(); System.out.println(lineFromServer); System.out.println(); incoming.close(); } catch (Exception e) { System.out.println("Error: " + e); } } // end main() } //end class DateClient حيث: [1]: يَفتَح هذا البرنامج اتصالًا مع الحاسوب المُخصَّص مثل وسيطٍ أول بسطر الأوامر؛ وإذا لم يكن مخصَّصًا بعد، اطلب من المُستخدِم أن يُدْخِل الحاسوب الذي يرغب في الاتصال به. يُجرى البرنامج الاتصال على رقم المنفذ LISTENING_PORT، ويقرأ سطرًا نصيًا واحدًا من الاتصال ثم يُغلق الاتصال، ويُرسِل أخيرًا النص المقروء إلى الخرج القياسي. الهدف من هذا البرنامج هو استخدامه مع البرنامج DateServer الذي يُرسِل كُلًا من التوقيت والتاريخ الحاليين بالحاسوب الذي يَعمَل عليه الخادم. [2]: أعاد التابع incoming.readLine() القيمة الفارغة مما يُعدّ إشارةً إلى وصوله إلى نهاية المجرى. لاحِظ أن الشيفرة المسؤولة عن الاتصال مع الخادم مُضمَّنةٌ داخل تعليمة try..catch، حتى تَلتقِط استثناءات النوع IOException، والتي يُحتمَل وقوعها أثناء فتح الاتصال أو غلقه أو قراءة البيانات من مجرى الدْخَل. أحطنا مجرى الدْخَل الخاص بالاتصال بكائن من النوع BufferedReader، والذي يحتوي على التابع readLine()؛ وهذا يُسهّل قراءة سطرٍ واحدٍ من المُدْخَلات. إذا أعاد التابع القيمة null، يكون الخادم قد أغلق الاتصال دون أن يُرسِل أي بيانات. حتى يَعمَل هذا البرنامج دون أخطاء، ينبغي أن تُشغِّل برنامج الخادم أولًا على الحاسوب الذي يُحاوِل العميل الاتصال به، حيث يُمكِنك أن تُشغِّل برنامجي العميل والخادم على نفس الحاسوب. على سبيل المثال، افتح نافذتي سطر أوامر، ثم شغِّل الخادم بإحداها، وشغِّل العميل بالنافذة الأخرى. علاوةً على ذلك، تَستطِيع أغلب الحواسيب استخدام اسم المجال "localhost" وعنوان IP التالي "127.0.0.1" للإشارة الى ذاتها، ولهذا يُمكِنك استخدام الأمر java DateClient localhost لتطلب من البرنامج DateClient الاتصال مع الخادم المُشغَّل على نفس الحاسوب؛ وإذا لم يَنجَح معك الأمر، جرِّب الأمر java DateClient 127.0.0.1. أطلقنا اسم DateServer على برنامج الخادم المقابل لبرنامج العميل DataClient، حيث يُنشِئ برنامج DateServer مقبسًا socket من النوع ServerSocket للاستماع إلى طلبات الاتصال برقم المنفذ "32007". ويَستمِر بعد ذلك بتنفيذ حلقة تكرار loop لا نهائية تَستقبِل طلبات الاتصال وتُعالِجها. تستمر حلقة التكرار بالعمل حتى ينتهي البرنامج بطريقةٍ ما، مثل كتابة CONTROL-C بنافذة سطر الأوامر التي شغَّلت الخادم منها. عندما يَستقبِل الخادم طلب اتصالٍ من عميلٍ معين، فإنه يَستدعِي برنامجًا فرعيًا لمعالجة هذا الاتصال، حيث يلتقط البرنامج الفرعي أي استثناءات من النوع Exception حتى لا ينهار الخادم، وهذا أمرٌ منطقي؛ فلا ينبغي للخادم أن يُغلَق لمجرد أن اتصالًا واحدًا مع عميل معين قد فشل لسببٍ ما؛ فقد يكون العميل هو سبب الخطأ أساسًا. بخلاف التقاطه للاستثناءات، فإنه يُنشِئ كائنًا من النوع PrintWriter لإرسال البيانات عبر الاتصال، ويُرسِل تحديدًا التاريخ والتوقيت الحالي إلى ذلك المجرى، ثم يُغلِق الاتصال؛ حيث يَستخدِم البرنامج الصنف القياسي java.util.Date للحصول على التوقيت الحالي، وتُمثِل كائنات الصنف Date تاريخًا وتوقيتًا محددًا، ويُنشِئ الباني الافتراضي new Date() كائنًا يُمثِّل توقيت إنشائه. انظر الشيفرة الكاملة لبرنامج الخادم: import java.net.*; import java.io.*; import java.util.Date; // 1 public class DateServer { public static final int LISTENING_PORT = 32007; public static void main(String[] args) { ServerSocket listener; // يستمع إلى طلبات الاتصال Socket connection; // للتواصل مع البرنامج المتصل // 2 try { listener = new ServerSocket(LISTENING_PORT); System.out.println("Listening on port " + LISTENING_PORT); while (true) { // استقبل طلب الاتصال التالي وعالِجه connection = listener.accept(); sendDate(connection); } } catch (Exception e) { System.out.println("Sorry, the server has shut down."); System.out.println("Error: " + e); return; } } // end main() // 3 private static void sendDate(Socket client) { try { System.out.println("Connection from " + client.getInetAddress().toString() ); Date now = new Date(); // التوقيت والتاريخ الحالي PrintWriter outgoing; // مجرى لإرسال البيانات outgoing = new PrintWriter( client.getOutputStream() ); outgoing.println( now.toString() ); outgoing.flush(); // تأكّد من إرسال البيانات client.close(); } catch (Exception e){ System.out.println("Error: " + e); } } // end sendDate() } //end class DateServer حيث: [1] يُمثِّل هذا البرنامج خادمًا يستمع إلى طلبات الاتصال على رقم المَنفَذ المخصَّص بواسطة الثابت LISTENING_PORT. عند فتح اتصال، يُرسِل البرنامج التوقيت الحالي إلى المقبس المُتصِل، ويستمر البرنامج في تسلُّم طلبات الاتصال ومعالجتها حتى يُغلِق عن طريق الضغط على CONTROL-C على سبيل المثال. ملاحظة: يعالِج الخادم طلبات الاتصال عند وصولها بدلًا من إنشاء خيطٍ thread منفصلٍ لمعالجتها. [2] اِستقبِل طلبات الاتصال وعالِجها للأبد أو إلى حين وقوع خطأ. ملاحظة: يلتقط البرنامج sendDate() الأخطاء الواقعة أثناء نقل البيانات مع برنامجٍ مُتصِل ويعالجها حتى لا ينهار الخادم. [3] يُمثِّل المعامل client مقبسًا متصلًا بالفعل مع برنامجٍ آخر، لذلك احصل على مجرى خرج لهذا الاتصال، وأرسل إليه التوقيت الحالي، ثم أغلق الاتصال. عندما تُشغِّل البرنامج DateServer بواجهة سطر الأوامر، فسيستمر بانتظار طلبات الاتصال ويُبلِّغ عنها عندما يتسلّمها. في المقابل، إذا أردت إتاحته على الحاسوب باستمرار، فينبغي أن تُشغِّله مثل عفريت daemon (وهو أمرٌ لن نناقش طريقة تنفيذه هنا)؛ وهو مثل برنامجٍ يَعمَل خفيةً وباستمرار على الحاسوب بغض النظر عن المُستخدِم. يُمكِنك ضبط الحاسوب ليبدأ بتشغيل هذا البرنامج تلقائيًا بمجرد أن تُشغِّله -أي الحاسوب-، وسيَعمَل بذلك البرنامج بالخلفية حتى إذا كنت تَستخدِم الحاسوب لأغراضٍ أخرى. تُشغِّل الحواسيب المسؤولة عن إتاحة بعض الصفحات على شبكة الويب العالمية مثلًا عفريتًا يستمع إلى طلبات العملاء لتلك الصفحات ثم ينقلها إليهم، ويشبه ذلك البرنامج DateServer، كما يُمكِن تشغيله يدويًا ببساطة لاختباره؛ فكل تلك الأمثلة بالنهاية غير متينة كفاية ولا تتمتع بخاصياتٍ مكتملة كفاية لتشغيلها خوادمًا فعلًا. بالمناسبة، تُعدّ كلمة "daemon" تهجئةً مختلفةً لكلمة "demon" وتُنطَق بنفس الطريقة. ملاحظة: بعد أن يَستدعِي البرنامج التابع outgoing.println() لإرسال سطرٍ واحدٍ من البيانات إلى العميل؛ يَستدعِي أيضًا التابع outgoing.flush() المُتاح بجميع أصناف مجاري الخرج، ليتأكَّد من إرسال البيانات التي كتبها بالمجرى بالفعل إلى مقصدها. في العموم، عليك استدعاء تلك الدالة بكل مرةٍ تَستخدِم بها مجرى خرج لإرسال بياناتٍ عبر اتصالٍ شبكي؛ لأنك إن لم تَفعَل ذلك، فقد يستمر المجرى بتخزين البيانات إلى أن يَصِل حجمها إلى القدر الكافي، ثم يُرسِلها بهدف رفع الكفاءة، ولكنه قد يؤدي إلى بعض التأخير غير المقبول إذا كان العميل ينتظر الرد، بل ربما حتى لا تُرسَل بعض البيانات عند غلق المقبس socket؛ ولذلك من الضروري استدعاء flush() قبل غلق أي اتصال. يُعدّ هذا واحدًا من الحالات التي قد تتصرف خلاله تنفيذات جافا المختلفة بطرائقٍ مختلفة. في العموم، إذا لم تستدعي التابع flush مع مجاري تدفق الخرج، فلربما سيَعمَل تطبيقك على بعض أنواع الحواسيب ولكنه قد لا يَعمَل أيضًا على بعضها الآخر. برنامج محادثة عبر الشبكة كان الخادم بالبرنامج السابق DateServer يُرسِل المعلومات إلى العميل ليقرأها. في الواقع، يُمكِننا أيضًا إنشاء اتصالٍ ثنائي الاتجاه بين العميل والخادم. سنفحص أولًا برنامجًا بسيطًا مُكوَّنًا من عميل وخادم؛ حيث سيُمكِّن البرنامج المُستخدِمين بطرفي الاتصال من إرسال الرسائل إلى بعضهما بعضًا. يَعمَل البرنامج ببيئة سطر الأوامر command-line environment، حيث يستطيع كل مُستخدِم كتابة رسائله، وسينتظر الخادم -بهذا المثال- طلب اتصالٍ من عميلٍ واحد فقط، ثم سيتوقَّف عن الاستماع إلى أي طلبات اتصال جديدة؛ أي لن يتمكَّن أي عميلٍ آخر غير العميل الأول من الاتصال بالخادم. بعد أن يَتصِل الخادم والعميل معًا، سيَعمَل البرنامج بكلا الطرفين بنفس الطريقة تقريبًا. سيَكتُب المُستخدِم بطرف العميل رسالةً ويُرسِلها إلى الخادم الذي سيَعرِضها بدوره إلى المُستخدِم بطرف الخادم، ثم سيَكْتُب المُستخدِم بطرف الخادم رسالةً لتنتقل بعدها إلى العميل الذي يُمكِنه كتابة رسالةٍ أخرى، وهكذا. سيستمر الأمر إلى أن يُقرِّر أي مُستخدِمٍ منهما كتابة كلمة "quit" برسالة؛ وعندما يحدث ذلك، يُغلَق الاتصال وينتهي البرنامج بكلا الطرفين. يتشابه برنامجا الخادم والعميل إلى حدٍ كبير، بينما يختلفان فقط بطريقة فتح الاتصال؛ فبرنامج العميل مُصمَّم ليُرسِل أول رسالةٍ؛ بينما يُصمَّم الخادم لاستقبالها. يُمكِنك الإطلاع على برنامجي الخادم والعميل بالملفين CLChatClient.java و CLChatServer.java، حيث يُشير الاسم "CLChat" إلى اختصارٍ لعبارة "command-line chat"، أي محادثة عبر سطر الأوامر. تَعرِض الشيفرة التالية برنامج الخادم (برنامج العميل مشابه): import java.net.*; import java.util.Scanner; import java.io.*; // 1 public class CLChatServer { // رقم المنفذ الافتراضي الذي ينبيغ الاستماع إليه إذا لم يُخصِّصه المُستخدم static final int DEFAULT_PORT = 1728; // 2 static final String HANDSHAKE = "CLChat"; // يَسبِق هذا المحرف جميع الرسائل المُرسَلة static final char MESSAGE = '0'; // يُرسَل هذا المحرف إلى البرنامج المتصل عندما يغلق المستخدم الاتصال static final char CLOSE = '1'; public static void main(String[] args) { int port; // رقم المنفذ الذي يستمع إليه الخادم ServerSocket listener; // يستمع إلى طلبات الاتصال Socket connection; // للتواصل مع العميل BufferedReader incoming; // مجرى لاستقبال البيانات من العميل PrintWriter outgoing; // مجرى لإرسال البيانات إلى العميل String messageOut; // رسالة ينبغي إرسالها إلى العميل String messageIn; // رسالة ينبغي استقبالها من العميل // مغلِّف للكائن System.in لقراءة أسطر مدخلة من المستخدم Scanner userInput; // 3 if (args.length == 0) port = DEFAULT_PORT; else { try { port= Integer.parseInt(args[0]); if (port < 0 || port > 65535) throw new NumberFormatException(); } catch (NumberFormatException e) { System.out.println("Illegal port number, " + args[0]); return; } } // 4 try { listener = new ServerSocket(port); System.out.println("Listening on port " + listener.getLocalPort()); connection = listener.accept(); listener.close(); incoming = new BufferedReader( new InputStreamReader(connection.getInputStream()) ); outgoing = new PrintWriter(connection.getOutputStream()); outgoing.println(HANDSHAKE); // Send handshake to client. outgoing.flush(); messageIn = incoming.readLine(); // Receive handshake from client. if (! HANDSHAKE.equals(messageIn) ) { throw new Exception("Connected program is not a CLChat!"); } System.out.println("Connected. Waiting for the first message."); } catch (Exception e) { System.out.println("An error occurred while opening connection."); System.out.println(e.toString()); return; } // 5 try { userInput = new Scanner(System.in); System.out.println("NOTE: Enter 'quit' to end the program.\n"); while (true) { System.out.println("WAITING..."); messageIn = incoming.readLine(); if (messageIn.length() > 0) { // 6 if (messageIn.charAt(0) == CLOSE) { System.out.println("Connection closed at other end."); connection.close(); break; } messageIn = messageIn.substring(1); } System.out.println("RECEIVED: " + messageIn); System.out.print("SEND: "); messageOut = userInput.nextLine(); if (messageOut.equalsIgnoreCase("quit")) { // 7 outgoing.println(CLOSE); outgoing.flush(); // تأكّد من إرسال البيانات connection.close(); System.out.println("Connection closed."); break; } outgoing.println(MESSAGE + messageOut); outgoing.flush(); // تأكَّد من إرسال البيانات if (outgoing.checkError()) { throw new IOException("Error occurred while transmitting message."); } } } catch (Exception e) { System.out.println("Sorry, an error has occurred. Connection lost."); System.out.println("Error: " + e); System.exit(1); } } // end main() } //end class CLChatServer حيث: [1] يُمثِّل هذا البرنامج أحد طرفي برنامج محادثةٍ بسيط عبر سطر الأوامر، حيث يَعمَل البرنامج كأنه خادم ينتظر طلبات الاتصال من البرنامج CLChatClient. يُمكِن تخصيص رقم المنفذ الذي يستمع إليه الخادم مثل وسيط بسطر الأوامر؛ ويَستخدِم البرنامج في حالة عدم تخصيصه رقم المنفذ الافتراضي المُخصَّص عبر الثابت DEFAULT_PORT. ملاحظة: يستمع الخادم إلى أي رقم منفذٍ متاح، في حالة تخصيص العدد صفر رقمًا للمنفذ. يدعم هذا البرنامج اتصالًا واحد فقط؛ فبمجرد فتح الاتصال، يتوقف مقبس الاستماع، ويُرسِل طرفي الاتصال بعد ذلك رسالةً نصيةً لتحقيق الاتصال إلى بعضهما بعضًا، ليتأكّد كلا الطرفين من أن البرنامج على الطرف الآخر من النوع الصحيح، وفي تلك الحالة، يبدأ البرنامجان المتصلان بتبادل الرسائل. لا بُدّ أن يُرسِل برنامج العميل الرسالة الأولى، وبإمكان المُستخدِم بأيٍّ من الطرفين إغلاق الاتصال بإدخال السلسلة النصية "quit". ملاحظة: لا بُدّ أن يكون المحرف الأول بأي رسالةٍ نصيةٍ مُرسَلة عبر الشبكة مساويًا للقيمة "0" أو "1"، حيث يُفسَّر على أنه أمر. [2] سلسلة نصية لتحقيق الاتصال؛ حيث يرسل طرفي الاتصال تلك الرسالة النصية إلى بعضهما بمجرد فتح الاتصال لنتأكّد من أن الطرف الآخر هو برنامج CLChat. [3] اقرأ رقم المنفذ من سطر الأوامر أو اِستخدِم رقم المنفذ الافتراضي إذا لم يُخصِّصه المُستخدم. [4] انتظر طلب اتصال؛ وعندما يَصِل طلب اتصال، أغلق المستمع، وأنشِئ مجاري تدفق لتبادل البيانات والتحقق من الاتصال. [5] تبادل الرسائل مع الطرف الآخر من الاتصال حتى يُغلِق أحدهما الاتصال. ينتظر لخادم الرسالة الأولى من العميل، ويتبادل بعد ذلك الطرفان الرسائل جيئةً وذهابًا. [6] يعد المِحرف الأول من الرسالة أمرًا. إذا كان الأمر هو إغلاق الاتصال، أغلقه؛ أما إذا لم يَكن كذلك، اِحذِف محرف الأمر من الرسالة وأكمل المعالجة. [7] يرغب المُستخدِم بإغلاق الاتصال. بلِّغ الطرف الآخر وأغلق الاتصال. يُعدّ هذا البرنامج أكثر متانةً robust نوعًا ما من البرنامج DateServer؛ لأنه يُجرِي تحقيق اتصال handshake ليتأكّد من أن العميل الذي يحاول الاتصال به هو بالفعل البرنامج CLChatClient. يُجرَى تحقيق الاتصال بتبادل بعض المعلومات بين العميل والخادم على أنه جزءٌ من عملية إنشاء الاتصال قبل إرسال أي بياناتٍ فعلية، ويُرسِل طرفا الاتصال في تلك الحالة سلسلةً نصيةً إلى الطرف الآخر لتعريف هويته؛ حيث يُعدّ تحقيق الاتصال جزءًا من بروتوكول إجراء اتصال -أنشأه الكاتب- بين البرنامجين CLChatClient وCLChatServer. يُعدّ أي بروتوكول توصيفًا مفًصَّلًا لما يُمكِن تبادله من بياناتٍ ورسائلٍ عبر اتصالٍ معين، وكذلك طريقة تمثيل تلك البيانات، وبأي ترتيبٍ ينبغي إرسالها. ويُعدّ تصميم البروتوكول جانبًا مهمًا بتطبيقات الخوادم/العملاء. ينطوي بروتوكول "CLChat" على جانبٍ آخر بالإضافة إلى تحقيق الاتصال؛ حيث يَنُصّ على أن المحرف الأول بأي سطرٍ نصي يُرسَل عبر الاتصال هو أمر. إذا كان المِحرف الأول يُساوِي "0"، فيُمثِّل السطر رسالةً من مُستخدمٍ لآخر؛ أما إذا كان يُساوِي "1"، فسيُشير السطر إلى أن أحدهما قد أدخَل الأمر "quit"، مما يؤدي إلى غلق الاتصال. ترجمة -بتصرّف- للقسم Section 4: Networking من فصل Chapter 11: Input/Output Streams, Files, and Networking من كتاب Introduction to Programming Using Java. اقرأ أيضًا المقال السابق: معالجة الملفات في جافا الإنترنت وما بعده وعلاقته بجافا واجهة المستخدم الحديثة في جافا فهم نموذج التواصل بين المضيفين في الشبكات1 نقطة
-
سنفحص خلال هذا القسم بعض الأمثلة البرمجية على تعامل البرامج مع الملفات باستخدام التقنيات المُوضحَّة التي عرضناها في المقالين السابقين، مقال قنوات الدخل والخرج وعمليتي القراءة والكتابة في جافا ومقال مدخل إلى التعامل مع الملفات في جافا. نسخ الملفات سنناقش الآن برنامج سطر أوامرٍ بسيط لنسخ الملفات، حيث تُعدّ عملية نسخ الملفات واحدةً من أكثر العمليات شيوعًا، ولهذا تُوفِّر جميع أنظمة التشغيل أمرًا مُخصَّصًا لتلك العملية بالفعل، ولكن ما يزال من المفيد من الناحية التعليمية أن ننظر إلى طريقة تنفيذ ذلك باستخدام برنامج جافا. تتشابه غالبية العمليات على الملفات مع عملية نسخ ملف، باستثناء اختلاف طريقة معالجتها لبيانات الملف المُدْخَلة قبل إعادة كتابتها إلى ملف الخرج؛ أي يُمكِن كتابة برامج لكل تلك العمليات بنفس الكيفية تقريبًا. تعرَّضنا في فصل المعاملات (parameters) في جافا لبرنامجٍ يَستخدِم الصنف TextIO لنسخ ملفاتٍ نصية، بينما يَعمَل البرنامج بالأسفل مع جميع أنواع الملفات. ينبغي أن يتمكَّن البرنامج من نسخ أي ملف، وبالتالي لا يُمكِن للملف أن يكون بصيغةٍ مقروءة، وهذا يَعنِي أننا سنضطّر لمعالجته باستخدام أصناف مجاري البايتات InputStream و OutputStream. يَنسَخ البرنامج البيانات من مجرًى من النوع InputStream إلى مجرًى آخر من النوع OutputStream، بحيث يَنسَخ بايتًا واحدًا في كل مرة. إذا كان source متغيرًا يُشير إلى مجرى الدخل من الصنف InputStream، فستقرأ الدالة source.read() بايتًا واحدًا. تعيد تلك الدالة القيمة "-1" بعد الانتهاء من قراءة كلِّ البايتات الموجودة بملف الدْخَل. بالمثل، إذا كان copy مُتغيّرًا يُشير إلى مجرى الخرج من الصنف OutputStream، فستكتب الدالة copy.write(b) بايتًا واحدًا في ملف الخرج. يُمكِننا بناءً على ما سبق كتابة البرنامج بهيئة حلقة while محاطةً بتعليمة try..catch؛ نظرًا لإمكانية عمليات الدْخَل والخرج في التبليغ عن اعتراضات: while(true) { int data = source.read(); if (data < 0) break; copy.write(data); } يَستقبِل أمر نسخ الملفات بنظام تشغيل، مثل UNIX وسطاء سطر الأوامر command line arguments لتخصيص أسماء الملفات المطلوبة، حيث يستطيع المُستخدِم مثلًا كتابة أمرٍ، مثل copy original.dat backup.dat، لينسخ ملفًا موجودًا اسمه "original.dat" إلى ملفٍ اسمه "backup.dat". تستطيع برامج جافا استخدام وسطاء سطر الأوامر بنفس الطريقة؛ حيث تُخزَّن قيمها ضمن مصفوفةٍ من السلاسل النصية اسمها args، والتي يَستقبِلها البرنامج main() مثل معاملٍ، ويستطيع بذلك البرنامج استرجاع القيم المُمرَّرة للوسطاء (انظر فصل المعاملات (parameters) في جافا). على سبيل المثال، إذا كان "CopyFile" هو اسم البرنامج، وشَغّله المُستخدِم بكتابة الأمر التالي: java CopyFile work.dat oldwork.dat فستُساوِي قيمة args[0] بالبرنامج السلسلة النصية "work.dat"؛ أما قيمة args[1] فستُساوِي السلسلة النصية "oldwork.dat". تُشير قيمة args.length إلى عدد الوسطاء المُمرَّرين. يَحصُل برنامج CopyFile.java على اسمي الملفين من خلال وسطاء سطر الأوامر، ويَطبَع رسالة خطأ إن لم يَجِدهما. هناك طريقتان لاستخدام البرنامج، هما: أولًا، قد يحتوي سطر الأوامر ببساطةٍ على اسمي ملفين، ويَطبَع البرنامج في تلك الحالة رسالة خطأ وينتهي إذا كان ملف الخرج المُخصَّص موجودًا مُسبقًا؛ لكي لا يَكْتُب بملفٍ مهمٍ عن طريق الخطأ. ثانيًا، قد يحتوى سطر الأوامر على ثلاثة وسطاء، ولا بُدّ في تلك الحالة أن يكون الوسيط الأول هو الخيار "-f"؛ أما الثاني والثالث فهما اسما الملفين. تُعدّل كتابة الوسيط "-f" من سلوك البرنامج، حيث يُفسِّره البرنامج على أنه رُخصةً للكتابة بملف الخرج حتى لو كان موجودًا مُسبقًا. لاحِظ أن "-f" هي في الواقع اختصار لكلمة "force"؛ نظرًا لأنها تجبر البرنامج على نسخ الملف حتى في الحالات التي كان البرنامج سيتعامل معها كما لو كانت خطأً بصورةٍ افتراضية. يُمكِنك الاطلاع على شيفرة البرنامج لترى طريقة تفسيره لوسطاء سطر الأوامر: import java.io.*; // 1 public class CopyFile { public static void main(String[] args) { String sourceName; // اسم ملف المصدر كما خُصّص بسطر الأوامر String copyName; // اسم ملف النسخة المُخصَّص InputStream source; // مجرًى للقراءة من ملف المصدر OutputStream copy; // مجرًى للكتابة بنسخة الملف // اضبطها إلى القيمة true إذا كان الخيار "f-" موجودًا بسطر الأوامر boolean force; int byteCount; // عدد البايتات المنسوخة حتى الآن // 2 if (args.length == 3 && args[0].equalsIgnoreCase("-f")) { sourceName = args[1]; copyName = args[2]; force = true; } else if (args.length == 2) { sourceName = args[0]; copyName = args[1]; force = false; } else { System.out.println( "Usage: java CopyFile <source-file> <copy-name>"); System.out.println( " or java CopyFile -f <source-file> <copy-name>"); return; } /* أنشئ مجرى الدخل، وأنهِ البرنامج في حالة حدوث خطأ */ try { source = new FileInputStream(sourceName); } catch (FileNotFoundException e) { System.out.println("Can't find file \"" + sourceName + "\"."); return; } // 4 File file = new File(copyName); if (file.exists() && force == false) { System.out.println( "Output file exists. Use the -f option to replace it."); return; } /* أنشئ مجرى الخرج وأنهِ البرنامج في حالة حدوث خطأ */ try { copy = new FileOutputStream(copyName); } catch (IOException e) { System.out.println("Can't open output file \"" + copyName + "\"."); return; } // 3 byteCount = 0; try { while (true) { int data = source.read(); if (data < 0) break; copy.write(data); byteCount++; } source.close(); copy.close(); System.out.println("Successfully copied " + byteCount + " bytes."); } catch (Exception e) { System.out.println("Error occurred while copying. " + byteCount + " bytes copied."); System.out.println("Error: " + e); } } // end main() } // end class CopyFile حيث يُقصد بـ: [1]: أنشئ نسخةً من ملف. يجب تخصيص كُلٍ من اسم الملف الأصلي، واسم ملف النسخة على أنهما وسائطٌ بسطر الأوامر. يُمكِننا بالإضافة إلى ذلك كتابة الخيار "-f" على أنه وسيطٌ أول، وسيكتب البرنامج في تلك الحالة على الملف الذي يحمل اسم ملف النسخة في حالة وجوده مسبقًا؛ أما إذا لم يكن هذا الخيار موجودًا، فسيبلِّغ البرنامج عن خطأ وينتهي إذا كان الملف موجودًا. يُبلِّغ البرنامج أيضًا عن عدد البايتات التي نسخها من الملف. [2]: احصل على أسماء الملفات من سطر الأوامر وافحص فيما إذا كان الخيار "-f" موجودًا. إذا لم يكن الأمر بأيٍّ من الصيغ المحتملة، اطبع رسالة خطأ وأنهِ البرنامج. [3]: اِنسَخ بايتًا واحدًا بكل مرة من مجرى الدخل إلى مجرى الخرج حتى يعيد التابع read() القيمة "-1"، والتي تُعدّ إشارةً إلى الوصول إلى نهاية المجرى. إذا حدث خطأٌ، اطبع رسالة خطأ، وكذلك اطبع رسالةً في حالة نسخ الملف بنجاح. [4]: إذا كان ملف الخرج موجودًا بالفعل، ولم يُخصِّص المُستخدِم الخيار "-f"، اطبع رسالة خطأ وأنهِ البرنامج. لا تَعمَل عملية نسخ بايتٍ واحدٍ بكل مرة بالكفاءة المطلوبة، حيث يُمكِن تحسينها باستخدام نسخٍ أخرى من التابعين read() و write()، والتي بإمكانها قراءة وكتابة عدة بايتات بنفس الوقت (انظر واجهة برمجة التطبيقات لمزيدٍ من التفاصيل). يُمكننا بدلًا من ذلك أن نحيط مجاري تدفق الدخل والخرج بكائناتٍ من النوع BufferedInputStream و BufferedOutputStream، والتي يُمكِنها قراءة أو كتابة كتلٍ من البيانات من وإلى الملف مباشرةً، ويتطلّب ذلك تعديل سطرين فقط من البرنامج المسؤول عن إنشاء مجاري التدفق. فمثلًا، يُمكِننا أن نُنشِئ مجرى الدخل على النحو التالي: source = new BufferedInputStream(new FileInputStream(sourceName)); وبذلك يُمكِننا استخدام المجرى المُدعَّم بخاصية التخزين المؤقت buffered stream بنفس طريقة استخدام المجرى العادي. يُمكِنك الإطلاع على البرنامج التوضيحي CopyFileAsResources.java، والذي يُنجز نفس مهمة البرنامج CopyFile، ولكنه يَستخدِم نمط المورد resource pattern ضمن تعليمة try..catch؛ ليتأكَّد من غلق المجاري بجميع الحالات، وهو ما ناقشناه بنهاية القسم "تعليمة Try" من فصل الاستثناءات exceptions وتعليمة try..catch في جافا. البيانات الدائمة بمجرد انتهاء برنامجٍ معينٍ من العمل، تُلغَى جميع البيانات التي خزَّنها البرنامج بمتغيراتٍ أو كائناتٍ أثناء تنفيذه، مع أننا قد نرغب أحيانًا في الإبقاء على بعض من تلك البيانات بحيث تظل متاحةً للبرنامج عند تنفيذه مرةً أخرى. يطرح ذلك السؤال التالي: كيف يُمكِننا الاحتفاظ بالبيانات وإتاحتها للبرنامج مرةً أخرى؟ الإجابة ببساطة هي بتخزينها بملف، أو قاعدة بيانات database لبعض التطبيقات، رغم أننا إذا شئنا الدقة فهي تُعدّ ملفات أيضًا؛ حيث تكون البيانات الموجودة ضمن قاعدة بيانات مُخزَّنةً بالنهاية ضمن ملفات. لنأخذ مثالًا على ذلك، وهو برنامج "دليل هاتف" يَسمَح للمُستخدِم بالاحتفاظ بقائمةٍ من الأسماء وأرقام الهواتف. لن يكون للبرنامج أي معنًى إذا اضطّر المُستخدِم لإعادة إنشاء القائمة من الصفر بكل مرةٍ يُشغِّل فيها البرنامج، وإنما ينبغي أن نُفكِر بدليل الهاتف كما لو أنه تجميعةٌ دائمة persistent من البيانات، وأن نفكر بالبرنامج على أنه مجرد واجهةٍ لتلك التجميعة. سيَسمَح البرنامج للمُستخدِم بالبحث بدليل الهاتف من خلال الاسم، وكذلك بإدخال بياناتٍ جديدة. وينبغي بالطبع الاحتفاظ بأي تغييراتٍ يُجريها المُستخدِم لما بعد انتهاء البرنامج. يُعد البرنامج PhoneDirectoryFileDemo.java تنفيذًا implementation بسيطًا لتلك الفكرة. لاحِظ أنه صُمِّم ليكون فقط مثالًا على طريقة توظيف الملفات ضمن برنامج، فلا تُحملّه أكثر من حجمه، فهو ليس برنامجًا حقيقيًا. يُخزِّن البرنامج بيانات دليل الهاتف بملفٍ اسمه ".phonebookdemo" بالمجلد الرئيسي للمُستخدِم، والذي يُحدِّده البرنامج بالاستعانة بالتابع System.getProperty() الذي ذكرناه في مقال مدخل إلى التعامل مع الملفات في جافا المشار إليه في الأعلى. عندما يبدأ البرنامج بالعمل، فإنه يَفحَص أولًا فيما إذا كان الملف موجودًا بالفعل؛ فإذا كان موجودًا، فإنه يحتوي بالضرورة على بيانات دليل الهاتف الخاصة بالمُستخدِم، والتي خُزِّنت أثناء تشغيله لنفس البرنامج بمرةٍ سابقة، ويقرأ البرنامج في تلك الحالة بيانات الملف، ويُخزِّنها بكائنٍ اسمه phoneBook من النوع TreeMap؛ حيث يُمثِّل هذا الكائن دليل الهاتف أثناء تشغيل البرنامج (انظر القسم "واجهة تمثيل الخرائط" من فصل الخرائط Maps في جافا). علينا الآن الاتفاق على طريقة تمثيل بيانات دليل الهاتف قبل تخزينها بملف. سنختار تمثيلًا بسيطًا، يُمثِّل فيه كل سطرٍ ضمن الملف مُدْخَلًا واحدًا مكوَّنًا من اسم ورقم هاتف يَفصِل بينهما علامة النسبة المئوية %. تقرأ الشيفرة التالية ملف بيانات دليل الهاتف إذا كان موجودًا ومكتوبًا وفقًا لطريقة التمثيل المُتفَق عليها: File userHomeDirectory = new File( System.getProperty("user.home") ); File dataFile = new File( userHomeDirectory, ".phone_book_data" ); // A file named .phone_book_data in the user's home directory. if ( ! dataFile.exists() ) { System.out.println("No phone book data file found. A new one"); System.out.println("will be created, if you add any entries."); System.out.println("File name: " + dataFile.getAbsolutePath()); } else { System.out.println("Reading phone book data..."); try( Scanner scanner = new Scanner(dataFile) ) { while (scanner.hasNextLine()) { // اقرأ سطرًا واحدًا من الملف يحتوي على زوج اسم ورقم هاتف String phoneEntry = scanner.nextLine(); int separatorPosition = phoneEntry.indexOf('%'); if (separatorPosition == -1) throw new IOException("File is not a phonebook data file."); name = phoneEntry.substring(0, separatorPosition); number = phoneEntry.substring(separatorPosition+1); phoneBook.put(name,number); } } catch (IOException e) { System.out.println("Error in phone book data file."); System.out.println("File name: " + dataFile.getAbsolutePath()); System.out.println("This program cannot continue."); System.exit(1); } } بعد ذلك، يَسمَح البرنامج للمُستخدِم بإجراء عدّة عملياتٍ على دليل الهاتف، بما في ذلك تعديل محتوياته؛ فإذا عدَّل المُستخدِم أيًا من بيانات دليل الهاتف بينما البرنامج مُشغَّل، فسيُجرى هذا التعديل فقط على كائن الصنف TreeMap. وعندما يحين موعد انتهاء البرنامج، يُمكِننا عندها كتابة تلك البيانات المُعدَّلة بالملف باستخدام الشيفرة التالية: if (changed) { System.out.println("Saving phone directory changes to file " + dataFile.getAbsolutePath() + " ..."); PrintWriter out; try { out = new PrintWriter( new FileWriter(dataFile) ); } catch (IOException e) { System.out.println("ERROR: Can't open data file for output."); return; } for ( Map.Entry<String,String> entry : phoneBook.entrySet() ) out.println(entry.getKey() + "%" + entry.getValue() ); out.flush(); out.close(); if (out.checkError()) System.out.println("ERROR: Some error occurred while writing data file."); else System.out.println("Done."); } ينتج عن ذلك أن جميع البيانات -بما في ذلك التعديلات التي أجراها المُستخدِم- ستكون متاحةً بالمرة التالية التي يُنفَّذ خلالها البرنامج. عرضنا بالأعلى شيفرة البرنامج المُتعلِّقة بمعالجة الملف فقط، ولكن يُمكِنك بالطبع الإطلاع على باقي أجزاء البرنامج من هنا. حفظ الكائنات بملف عندما نرغب بحفظ أي بياناتٍ ضمن ملف، يجب أن نقرّر أولًا صيغة تمثيل تلك البيانات. نظرًا لاتِّباع كُلٍ من برامج الخرج المسؤولة عن كتابة البيانات وبرامج الدْخَل المسؤولة عن قرائتها نفس الصيغة المُقرَّرة، فستُصبح الملفات قابلةً لإعادة الاستخدام. ربما سيكون البرنامج بذلك مكتوبًا كتابةً صحيحة correctness، ولكن لا يُعدّ ذلك الأمر الهام الوحيد، وإنما يجب أيضًا أن تكون طريقة تمثيل البيانات بالملفات متينة (انظر الفصل مقدمة إلى صحة البرامج ومتانتها في جافا). سنناقش طرائقًا مختلفةً لتمثيل نفس البيانات لفهم ما يعنيه ذلك. سيعتمد المثال الذي سنُناقشه على المثال SimplePaint2.java من القسم "البرمجة باستخدام ArrayList" من فصل مفهوم المصفوفات الديناميكية (ArrayLists) في جافا (قد ترغب بتشغّيله لكي تتذكَّر إمكانياته)، حيث يُمكِّن هذا البرنامج المُستخدِم من استخدام الفأرة لرسم بعض الرسوم، وسنُضيف إليه الآن إمكانية القراءة من والكتابة إلى ملف؛ وسيَسمَح ذلك للمُستخدِم بحفظ رسمة معينة بملف، وقراءتها لاحقًا من نفس الملف، مما يُمكِّنه من إكمال العمل عليها لاحقًا. يتطلّب ذلك حفظ جميع البيانات المُتعلّقة بالرسمة ضمن ملف، لكي يتمكَّن البرنامج من إعادة رسمها بالكامل مرةً أخرى بعد قراءته للملف الخاص بالرسمة. يُمكِنك الإطلاع على النسخة الأحدث من البرنامج بملف الشيفرة المصدرية SimplePaintWithFiles.java، والتي أضفنا إليها قائمة "File" تُنفِّذ الأمرين "Save" و "Open"؛ لحفظ بيانات البرنامج بملف وكذلك قراءة البيانات المحفوظة بملف مرةً أخرى إلى البرنامج على الترتيب. تتكوّن بيانات الرسمة من لون الخلفية، وقائمةً بالمنحنيات التي رسمها المُستخدِم. تَتكوَّن بيانات كل منحنًى منها من قائمة نقاطٍ من النوع Point2D المُعرَّف بحزمة javafx.geometry؛ فإذا كان pt متُغيِّرًا من النوع Point2D، فسيُعيد تابعي المُتغيّر pt.getX() و pt.getY() قيمًا من النوع double تُمثِّل إحداثيات تلك النقطة بالمستوى xy. يُمكِن تخصيص لون كل منحنًى على حدى، كما يُمكِن للمنحنى أن يكون "متماثلًا symmetric"؛ بمعنى أنه بالإضافة إلى رسم المنحنى نفسه، تُرسَم انعكاسات المنحنى الأفقية والرأسية أيضًا. تُخزَّن بيانات كل منحنًى ضمن كائنٍ من النوع CurveData المُعرَّف بالبرنامج على النحو التالي: // 1 private static class CurveData { Color color; // لون المنحنى boolean symmetric; // هل ينبغي رسم الانعكاسات الأفقية والرأسية؟ ArrayList<Point2D> points; // النقاط الموجودة على المنحنى } حيث أن [1] هو كائنٌ من النوع CurveData يُمثِّل البيانات المطلوبة لإعادة رسم إحدى المنحنيات التي رسمها المُستخدِم. سنَستخدِم قائمةً من النوع ArrayList<CurveData> لحمل بيانات جميع المنحنيات التي رَسَمَها المُستخدِم. لنفكر الآن بالطريقة التي سنَحفَظ بها بيانات الرسمة ضمن ملفٍ نصي. في العموم، علينا تخزين جميع البيانات الضرورية لإعادة رسم الرسمة بملف خرجٍ ووفقًا لصيغةٍ مُحدّدة. بعد ذلك، ينبغي أن يتبِّع التابع المسؤول عن قراءة الملف نفس الصيغة تمامًا أثناء قرائته للبيانات، حيث سيتعيّن عليه اِستخدَام تلك البيانات لإعادة بناء بنى البيانات data structures التي تُمثِّل نفس الرسمة بينما البرنامج مُشغَّل. سنضطّر عند كتابة البيانات إلى التعبير عنها باستخدام قيم بياناتٍ بسيطة، مثل سلسلةٍ نصية أو قيمةٍ تنتمي لأيٍّ من الأنواع الأساسية primitive types؛ حيث يُمكِننا مثلًا التعبير عن اللون باستخدام ثلاثة أعدادٍ تُمثِّل مكوّنات اللون الأحمر والأخضر والأزرق. قد تكون الفكرة الأولى التي تخطر بذهنك هو مجرد طباعة كل البيانات الضرورية وفقًا لترتيبٍ محدّد، وهي في الواقع ليست الفكرة الأفضل. لنفترض أن out كائنٌ من النوع PrintWriter المُستخدَم لكتابة البيانات بالملف، يُمكِننا إذًا كتابة ما يلي: Color bgColor = getBackground(); // اكتب لون الخلفية إلى الملف out.println( bgColor.getRed() ); out.println( bgColor.getGreen() ); out.println( bgColor.getBlue() ); out.println( curves.size() ); // اكتب عدد المنحنيات for ( CurveData curve : curves ) { // لكل منحنًى، اكتب ما يلي out.println( curve.color.getRed() ); // لون المنحنى out.println( curve.color.getGreen() ); out.println( curve.color.getBlue() ); out.println( curve.symmetric ? 0 : 1 ); // خاصية تماثل المنحنى out.println( curve.points.size() ); // عدد النقاط الموجودة على المنحنى for ( Point2D pt : curve.points ) { // إحداثيات كل نقطة out.println( pt.getX() ); out.println( pt.getY() ); } } سيتمكَّن التابع المسؤول عن معالجة الملف من قراءة بياناته، وإعادة إنشاء ما يُكافئها من بنية بيانات. إذا كان التابع يَستخدِم كائنًا من النوع Scanner، وليَكُن اسمه هو scanner لقراءة بيانات الملف، يُمكِننا إذًا كتابة ما يلي: Color newBackgroundColor; // اقرأ لون الخلفية double red = scanner.nextDouble(); double green = scanner.nextDouble(); double blue = scanner.nextDouble(); newBackgroundColor = Color.color(red,green,blue); ArrayList<CurveData> newCurves = new ArrayList<>(); int curveCount = scanner.nextInt(); // عدد المنحنيات المقروءة for (int i = 0; i < curveCount; i++) { CurveData curve = new CurveData(); double r = scanner.nextDouble(); // اقرأ لون المنحنى double g = scanner.nextDouble(); double b = scanner.nextDouble(); curve.color = Color.color(r,g,b); int symmetryCode = scanner.nextInt(); // اقرأ خاصية تماثل المنحنى curve.symmetric = (symmetryCode == 1); curveData.points = new ArrayList<>(); int pointCount = scanner.nextInt(); // عدد النقاط الموجودة على المنحنى for (int j = 0; j < pointCount; j++) { int x = scanner.nextDouble(); // اقرأ إحداثيات النقطة int y = scanner.nextDouble(); curveData.points.add(new Point2D(x,y)); } newCurves.add(curve); } curves = newCurves; // اضبط بنى البيانات الجديدة setBackground(newBackgroundColor); ينبغي أن يقرأ تابع الدْخَل البيانات الموجودة بالملف بنفس الترتيب الذي اِستخدَمه تابع الخرج أثناء كتابتها. في حين تَفِي تلك الطريقة بالغرض، لا يكون ملف البيانات الناتج مفهومًا للقارئ على الإطلاق، تمامًا كما لو كنا قد كتبنا الملف بالصيغة الثنائية binary format؛ فهو مكوّنٌ فقط من سلسلةٍ طويلةٍ من الأعداد. يجعل ذلك الملف هشًا؛ حيث سيؤدي أي تعديلٍ بسيطٍ على طريقة تمثيل البيانات ضمن إصدار أحدث من البرنامج، مثل إضافة خاصيةٍ جديدة إلى المنحنيات، إلى إهدار الملفات القديمة، إلا إذا وفَّر الملف معلومةً عن إصدار البرنامج المُستخدَم لإنشائه. ولهذا، قررنا الاعتماد على صيغة بياناتٍ أكثر تعقيدًا ولكنها ستُعطِي معنًى أكثر وضوحًا، فبدلًا من الاكتفاء بكتابة مجموعةٍ من الأعداد، اخترنا إضافة كلماتٍ إليها تُمثِّل معنى تلك الأعداد. نَعرِض فيما يلي مثالًا على ملف بيانات قصيرٍ نوعًا ما، لكنه يُبيّّن جميع الخاصيات المُدعَّمة حاليًا. ستتمكَّن غالبًا من فهم معناه بالكامل بمجرد قراءته: SimplePaintWithFiles 1.0 background 0.4 0.4 0.5 startcurve color 1 1 1 symmetry true coords 10 10 coords 200 250 coords 300 10 endcurve startcurve color 0 1 1 symmetry false coords 10 400 coords 590 400 endcurve يشير السطر الأول إلى البرنامج المسؤول عن إنشاء ملف البيانات، وسيتمكَّن بذلك البرنامج من إجراء اختبارٍ بسيطٍ على الملف الذي اختار المُستخدِم فتحَه، بفحص أول كلمةٍ موجودةٍ به، ويُمكِنه بناءً على ذلك التأكُّد مما إذا كان الملف من النوع الصحيح. بالإضافة إلى ذلك، يحتوي السطر الأول على رقم إصدار 1.0، والذي ينبغي أن يتغيّر إلى أرقام إصدارٍ أعلى في حال تغيّرت صيغة الملف في الإصدارات الأحدث من البرنامج. يستطيع البرنامج بذلك أن يَفحَص رقم إصدار الملف؛ فإذا كان البرنامج قادرًا على معالجة الملفات المكتوبة وفقًا للإصدار 1.0 فقط، ووجد أن صيغة الملف مكتوبةً وفقًا لإصدارٍ آخر، مثل 1.2، يُمكنِه أن يُوضِح للمُستخدِم أن عليه استخدام نسخةٍ أحدث من البرنامج ليتمكَّن من قراءة ملف البيانات ذاك. يُخصِّص السطر الثاني من البرنامج لون خلفية الصورة، وهو ما يَتضَح ببساطة من خلال كلمة "background" ببداية السطر، وتُمثِّل الأعداد الثلاثة مكوِّنات اللون الأحمر والأخضر والأزرق على الترتيب؛ بينما يُمثِّل الباقي من الملف بيانات المنحنيات المرسومة بالصورة. تَفصِل الكلمتان "startcurve" و "endcurve" بيانات كل منحنًى عن الآخر؛ والتي تتكوَّن من خاصيات اللون والتماثل وكذلك إحداثيات النقاط الواقعة على المنحنى. يُمكِننا إنشاء هذا النوع من الملفات يدويًا وتعديلها بسهولة، لأن معناها واضح، وقد أنشأنا ملف البيانات بالأعلى بواسطة محرر نصوص لا بواسطة البرنامج. يُمكِننا إضافة المزيد من الخيارات بسهولة؛ فقد تدعَم الإصدارات الأحدث من البرنامج مثلًا خاصية "السُمْك thickness" لرسم منحنياتٍ بخطوط عرضٍ مختلفة، ويُمكِنها أيضًا دعم رسم أشكالٍ أخرى، مثل المستطيلات والأشكال البيضاوية بنفس السهولة. من السهل أيضًا كتابة هذا النوع من البيانات عن طريق برنامج. لنفترض مثلًا أن out من النوع PrintWriter، وأننا سنَستخدِمه لكتابة بيانات الرسمة بملف، فستُجرِي الشيفرة التالية ذلك ببساطة: out.println("SimplePaintWithFiles 1.0"); // Version number. out.println( "background " + backgroundColor.getRed() + " " + backgroundColor.getGreen() + " " + backgroundColor.getBlue() ); for ( CurveData curve : curves ) { out.println(); out.println("startcurve"); out.println(" color " + curve.color.getRed() + " " + curve.color.getGreen() + " " + curve.color.getBlue() ); out.println( " symmetry " + curve.symmetric ); for ( Point2D pt : curve.points ) out.println( " coords " + pt.getX() + " " + pt.getY() ); out.println("endcurve"); } يَستخدِم التابع doSave() -ضمن هذا البرنامج- الشيفرة بالأعلى، وهو يُشبه كثيرًا التابع الذي عرضناه في الفصل السابق. لاحِظ أن هذا التابع يَستخدِم صندوق نافذة اختيار ملف ليَسمَح للمُستخدِم باختيار ملف الخرج. قد تكون قراءة بيانات الملف أصعب بعض الشيء؛ حيث ينبغي للبرنامج المسؤول عن قراءة الملف أن يتعامل مع كل تلك الكلمات الزائدة الموجودة به. اخترنا كتابة ذلك البرنامج بطريقةٍ تَسمَح بتبديل ترتيب ظهور البيانات ضمن الملف، فسيَسمَح البرنامج مثلًا بتخصيص لون الخلفية بنهاية الملف بدلًا من بدايته، كما سيَسمَح بعدم تخصيصها من الأساس، وسيَستخدِم البرنامج في تلك الحالة اللون الأبيض لونًا افتراضيًا للخلفية. تمكَّننا من إجراء ذلك بسبب عنونة كل عنصرٍ من البيانات بكلمةٍ تَصِف معناه، واعتمد البرنامج بالتالي على تلك الكلمات لاستنتاج ما ينبغي فعله. سيَقرأ ذلك التابع ملفات البيانات التي أنشأها التابع doSave()، وسيَستخدِم الصنف Scanner أثناء عملية القراءة. تَعرِض الشيفرة التالية شيفرة التابع بالكامل، والمُعرَّف ببرنامج SimplePaintWithFiles.java: private void doOpen() { FileChooser fileDialog = new FileChooser(); fileDialog.setTitle("Select File to be Opened"); fileDialog.setInitialFileName(null); // No file is initially selected. if (editFile == null) fileDialog.setInitialDirectory(new File(System.getProperty("user.home"))); else fileDialog.setInitialDirectory(editFile.getParentFile()); File selectedFile = fileDialog.showOpenDialog(window); if (selectedFile == null) return; // User canceled. Scanner scanner; try { scanner = new Scanner( selectedFile ); } catch (Exception e) { Alert errorAlert = new Alert(Alert.AlertType.ERROR, "Sorry, but an error occurred\nwhile trying to open the file."); errorAlert.showAndWait(); return; } try { String programName = scanner.next(); if ( ! programName.equals("SimplePaintWithFiles") ) throw new IOException("File is not a SimplePaintWithFiles data file."); double version = scanner.nextDouble(); if (version > 1.0) throw new IOException("File requires a newer version of SimplePaintWithFiles."); Color newBackgroundColor = Color.WHITE; ArrayList<CurveData> newCurves = new ArrayList<CurveData>(); while (scanner.hasNext()) { String itemName = scanner.next(); if (itemName.equalsIgnoreCase("background")) { double red = scanner.nextDouble(); double green = scanner.nextDouble(); double blue = scanner.nextDouble(); newBackgroundColor = Color.color(red,green,blue); } else if (itemName.equalsIgnoreCase("startcurve")) { CurveData curve = new CurveData(); curve.color = Color.BLACK; curve.symmetric = false; curve.points = new ArrayList<Point2D>(); itemName = scanner.next(); while ( ! itemName.equalsIgnoreCase("endcurve") ) { if (itemName.equalsIgnoreCase("color")) { double r = scanner.nextDouble(); double g = scanner.nextDouble(); double b = scanner.nextDouble(); curve.color = Color.color(r,g,b); } else if (itemName.equalsIgnoreCase("symmetry")) { curve.symmetric = scanner.nextBoolean(); } else if (itemName.equalsIgnoreCase("coords")) { double x = scanner.nextDouble(); double y = scanner.nextDouble(); curve.points.add( new Point2D(x,y) ); } else { throw new Exception("Unknown term in input."); } itemName = scanner.next(); } newCurves.add(curve); } else { throw new Exception("Unknown term in input."); } } scanner.close(); backgroundColor = newBackgroundColor; curves = newCurves; redraw(); editFile = selectedFile; window.setTitle("SimplePaint: " + editFile.getName()); } catch (Exception e) { Alert errorAlert = new Alert(Alert.AlertType.ERROR, "Sorry, but an error occurred while\ntrying to read the data:\n" + e); errorAlert.showAndWait(); } } لقد ناقشنا صيغ الملفات على هذا النحو المُفصَّل لنُحفِّزك على التفكير بمشكلة تمثيل البيانات المُعقّدة بصيغٍ يُمكِن تخزينها ضمن ملف، وسنتعرَّض لنفس المشكلة أثناء نقل البيانات عبر الشبكات. لا يُمكِننا في الواقع أن نقول أن حلًا معينًا هو الحل الصحيح لتلك المشكلة في العموم، ولكن بالطبع تُعدّ بعض الحلول أفضل من الأخرى، وسنُناقش في فصل لاحق واحدًا من أكثر الحلول شيوعًا لمشكلة تمثيل البيانات عمومًا. بالإضافة إلى قدرة البرنامج SimplePaintWithFiles على حفظ بيانات الرسوم بصيغٍ نصية، فإنه قادرٌ أيضًا على حفظها مثل ملفات صورٍ يُمكِن طباعتها أو وضعها بصفحة إنترنت على سبيل المثال. يُعدّ ذلك مثالًا عامًا على تقنيات معالجة الصور، والتي سنناقشها في جزئية لاحقة من هذه السلسلة، والتي تَستخدِم تقنيات أخرى لم نتعرَّض لها بعد. ترجمة -بتصرّف- للقسم Section 3: Programming With Files من فصل Chapter 11: Input/Output Streams, Files, and Networking من كتاب Introduction to Programming Using Java. اقرأ أيضًا المقال السابق: مدخل إلى التعامل مع الملفات في جافا كتابة أصناف وتوابع معممة في جافا الواجهات Interfaces في جافا التعاود recursion في جافا1 نقطة
-
1 نقطة
-
لغة البرمجة مهارة يسعى لتعلمها الكثير من الناس حول العالم خاصةً في الآونة الأخيرة, ومع ذلك فإن إكتساب هذه المهارة وتعلمها ليس بالأمر السهل, شعور بالإرهاق, حاجة دائمة لتعلم المزيد والمزيد.. معركة شاقة مع الوقت والتطورات المتسارعة. هنا بعض النصائح لمساعدتك على تعلم البرمجة بشكل أسرع. التفكير أولا أي لغة سوف تتعلم تحتاج قبل أن تخطو أولى خطواتك في تعلم البرمجة إلى اختيار لغة البرمجة التي ستتعلمها. لا توجد قاعدة حول المفاضلة بين لغة وأخرى, لكن, هنالك لغات قد تكون أسهل للتعلم وللبدء ، مثل Python و Java. إذا بدأت باستخدام لغة معقدة صعبة التعلم, قد تكون عائق في الإستمرار للوصول للهدف المطلوب. الممارسة المستمرة قراءة جميع الكتب البرمجية والإلتحاق بمئات الدورات ليس إلا خطوة بسيطة لتعلم البرمجة, إنما الممارسة المستمرة والأخطاء الكثيرة والبحث عن معرفة أسبابها ومعالجتها هي الطريق الأوحد لإحتراف البرمجة والإلتحاق بسوق العمل. البداية تبدو صعبة, لكن بالإستمرار تغدو ممتعة وجزء من النظام الحياة لا يمكن التخلي عنه. الأساسيات أولاً وثانياً تعلم الأساسيات وفهم منطق البرمجة ومخططات سير العمل من البداية هو الطرق الأفضل لفهم البرمجة والتعمق فيها. التواصل مع المجتمع التقني هناك العديد من المجموعات والصفحات على مواقع التواصل الإجتماعي تختص كل منها في تعلم لغة برمجة ما, الإنضمام لها ومتابعة الاقتراحات والمشاكل وحلولها, يساعدك أن التعلم في وقت قصير. فترات الراحة وتجديد النشاط العمل على شاشات الحواسيب لفترات طوال يرهق الجسد, مما يؤثر سلباً على مشوارك العلمي والعملي, تجديد النشاط وممارسة التمارين الرياضة مطلوب من جميع الناس خاصة العاملين بالوظائف المكتبية.1 نقطة
-
السلام عليكم, شخصيا لا أنصحك بالتفكير في التعلم بسرعة في أي مجال لأنه كلما تعلمت ببطء و إتقان تكون محترفا أكثر و متمكنا من المجال, و لأن التعلم السريع دائما لن يعود عليك بنفع بل فقط سيجعلك حائرا و يصعب عليك أداء المهام المطلوبة مستقبلا, لذلك أنصحك بأن تعطي لكل مجال وقته في التعلم, لأنه لا يوجد شيء ستصبح محترفا فيه بين ليلة و ضحاها.1 نقطة
-
السلام عليكم @ياسين بلقرع لتعلم البرمجة بسرعة عليك الانتباه لهذه النقاط: أول وأهم نقطة هو أنه يجب عليك تطبيق ما تتعلمه, هذا يساعد في فهم المصطلح جيدا. إجعل أهدافك واضحة حتى تتعلم التقنيات و اللغات التي تناسب المجال الذي تود أن تتعمق فيه. تعلم أساسيات اللغة جيدًا, لا تتسرع في الدخول إلى المواضيع المتقدمة و خذ وقت كافي .1 نقطة