JSP وJSTL وطبقة العرض

وسوم JSTL الأساسية

18 دقيقة الدرس 5 من 13

وسوم JSTL الأساسية

من أبرز أخطاء تطوير JSP في مراحله الأولى كان دمج كود Java مباشرةً داخل ملفات العرض (View) باستخدام Scriptlets. كان الناتج قوالبَ يستحيل تسليمها إلى مصمم، وعسيرة الاختبار، وهشّة في الصيانة. جاءت مكتبة وسوم JSP القياسية (JSTL) تحديداً لحل هذه المشكلة عبر استبدال Scriptlets بمجموعة من الوسوم شبيهة بـ XML. تغطّي مكتبة Core — ذات البادئة c: — الاحتياجات اليومية: الشروط، والتكرار، والمتغيرات ذات النطاق. أتقن هذه الوسوم الأربعة وستصبح ملفات JSP لديك مقروءة وقابلة للاختبار وودية للتصميم.

إضافة JSTL إلى مشروعك

JSTL ليست جزءاً من مواصفات Jakarta EE Servlet؛ بل تحتاج إلى إضافة ملف JAR للتنفيذ. مع Maven:

<!-- Jakarta EE 10 / Tomcat 10+ --> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency>

ثم أعلن عن مكتبة الوسوم Core في أعلى كل ملف JSP يستخدمها:

<%@ taglib uri="jakarta.tags.core" prefix="c" %>
فضاء اسم Jakarta مقابل javax: إن كنت على Tomcat 10 أو Jakarta EE 10 فأعلى، استخدم URI الخاص بـ jakarta.tags.core. على المشاريع القديمة مع Tomcat 9 أو Java EE يكون URI هو http://java.sun.com/jsp/jstl/core. تأكد دائماً من مطابقة إصدار JSTL لإصدار حاوية الـ Servlet.

c:set — تخزين القيم في النطاق

يُعدّ <c:set> البديل الـ JSTL لتعريف المتغيرات في الـ Scriptlet. يخزّن قيمة في أحد نطاقات JSP الأربعة: page أو request أو session أو application. النطاق الافتراضي هو page.

<%-- إنشاء متغير في نطاق الصفحة --%> <c:set var="greeting" value="Hello, World!" /> <%-- تخزين تعبير EL محسوب --%> <c:set var="itemCount" value="${cart.items.size()}" scope="request" /> <%-- تعيين محتوى النص الداخلي كقيمة --%> <c:set var="welcomeMsg"> Welcome back, ${user.firstName}! </c:set> <p>${greeting}</p> <p>You have ${itemCount} item(s) in your cart.</p>

من الأنماط المفيدة جداً استخدام c:set لإنشاء اسم بديل (alias) لخاصية متداخلة بعمق، حتى لا تضطر لتكرار مسار EL الكامل في كل أنحاء الصفحة.

استخدم c:set كاسم بديل: إن كنت تشير إلى ${order.customer.billingAddress.city} خمس مرات في الصفحة، عرّفه مرة واحدة — <c:set var="city" value="${order.customer.billingAddress.city}"/> — ثم استخدم ${city}. أقصر وأسرع (تقييم EL واحد) وأسهل في إعادة التسمية.

c:if — الشروط البسيطة

يعرض <c:if> محتواه الداخلي فقط عندما يُقيَّم الخاصية test بالقيمة true. وهو الشرط الأحادي الفرع البسيط.

<c:if test="${not empty user}"> <p>Welcome, <strong>${user.displayName}</strong>!</p> <a href="/logout">Log out</a> </c:if> <c:if test="${empty user}"> <a href="/login">Log in</a> </c:if> <%-- مقارنة رقمية --%> <c:if test="${product.stock gt 0}"> <button class="btn-add-to-cart">Add to Cart</button> </c:if> <c:if test="${product.stock eq 0}"> <span class="out-of-stock">Out of Stock</span> </c:if>

لاحظ عمليات EL: empty تتحقق من القيمة null أو المجموعة/النص الفارغ، وnot empty هي عكسها، وgt تعني أكبر من، وeq تعني يساوي. يُجنّبك استخدام هذه المشغّلات النصية الحاجةَ إلى هروب < و> داخل XML/HTML.

لا يملك c:if فرعاً else. إن احتجت بنية if/else، فأنت تحتاج c:choose. استخدام شرطَي c:if متعاكسَين هش — لو تغيّر الشرط عليك تحديث كلا الوسمَين وتخاطر بإدخال ثغرة أو تداخل.

c:choose — الشروط متعددة الفروع

يُعدّ <c:choose> مع <c:when> و<c:otherwise> المتداخلة المكافئ الـ JSTL لسلسلة switch/if-else. يُعرض فقط أول فرع when متطابق؛ أما otherwise فهو الاحتياطي حين لا يتطابق شيء.

<c:choose> <c:when test="${order.status eq 'PENDING'}"> <span class="badge badge-warning">Pending Review</span> </c:when> <c:when test="${order.status eq 'CONFIRMED'}"> <span class="badge badge-info">Confirmed</span> </c:when> <c:when test="${order.status eq 'SHIPPED'}"> <span class="badge badge-primary">Shipped</span> </c:when> <c:when test="${order.status eq 'DELIVERED'}"> <span class="badge badge-success">Delivered</span> </c:when> <c:otherwise> <span class="badge badge-secondary">${order.status}</span> </c:otherwise> </c:choose>

يؤدي فرع c:otherwise غرضاً مزدوجاً: يعرض قيمة افتراضية معقولة، ويعمل شبكةَ أمان حين تُضاف قيم enum جديدة في الكود الخلفي دون وجود c:when مقابل. أدرجه دائماً.

c:forEach — التكرار

يُعدّ <c:forEach> المكرّر في JSTL. يعمل على أي java.lang.Iterable والمصفوفات وإدخالات Map والنصوص المفصولة بفاصلة. وهو الوسم الأكثر استخداماً في تطبيقات الويب النموذجية.

عرض قائمة أساسي:

<table class="product-table"> <thead> <tr><th>Name</th><th>Price</th><th>Stock</th></tr> </thead> <tbody> <c:forEach var="product" items="${products}"> <tr> <td>${product.name}</td> <td>$${product.price}</td> <td>${product.stock}</td> </tr> </c:forEach> </tbody> </table>

الخاصية varStatus تكشف كائن حالة الحلقة مع خصائص مفيدة:

<c:forEach var="item" items="${cartItems}" varStatus="status"> <div class="cart-row ${status.last ? 'last-row' : ''}"> <span class="row-num">${status.count}</span> <%-- عداد يبدأ من 1 --%> <span>${item.product.name}</span> <span>${item.quantity} x $${item.product.price}</span> <c:if test="${not status.last}"> <hr class="divider"/> </c:if> </div> </c:forEach>

خصائص varStatus بإيجاز:

  • index — الفهرس الصفري للتكرار الحالي
  • count — العداد الذي يبدأ من 1 (index + 1)، مثالي لترقيم الصفوف
  • first — قيمة منطقية، تكون true في أول تكرار
  • last — قيمة منطقية، تكون true في آخر تكرار

التكرار على مدى أعداد صحيحة (دون الحاجة إلى مجموعة):

<%-- طباعة روابط ترقيم الصفحات من 1 إلى totalPages --%> <c:forEach var="page" begin="1" end="${totalPages}"> <a href="?page=${page}" class="${page eq currentPage ? 'active' : ''}">${page}</a> </c:forEach>

التكرار على Map:

<%-- خاصية الطلب: Map<String, Integer> categoryCounts --%> <ul> <c:forEach var="entry" items="${categoryCounts}"> <li>${entry.key}: ${entry.value} product(s)</li> </c:forEach> </ul>

الجمع بين الوسوم: مثال كامل

تجمع ملفات JSP الحقيقية بين هذه الوسوم الأربعة جميعاً. إليك مقتطفاً واقعياً من صفحة قائمة المنتجات يوضح كيف يضع Servlet البيانات في الطلب ثم يعرضها ملف JSP دون scriptlet واحد:

<%-- يضع Servlet: List<Product> products, String category, int totalCount --%> <c:set var="hasProducts" value="${not empty products}" /> <h1> <c:choose> <c:when test="${not empty category}"> ${category} Products </c:when> <c:otherwise>All Products</c:otherwise> </c:choose> </h1> <c:if test="${hasProducts}"> <p class="result-count">Showing ${products.size()} of ${totalCount}</p> <div class="product-grid"> <c:forEach var="p" items="${products}" varStatus="s"> <div class="product-card ${s.first ? 'featured' : ''}"> <img src="${p.imageUrl}" alt="${p.name}" /> <h3>${p.name}</h3> <p class="price">$${p.price}</p> <c:choose> <c:when test="${p.stock gt 10}"> <span class="in-stock">In Stock</span> </c:when> <c:when test="${p.stock gt 0}"> <span class="low-stock">Only ${p.stock} left!</span> </c:when> <c:otherwise> <span class="out-of-stock">Out of Stock</span> </c:otherwise> </c:choose> </div> </c:forEach> </div> </c:if> <c:if test="${not hasProducts}"> <p class="empty-state">No products found. <a href="/products">Browse all</a></p> </c:if>
فضّل مشغّلات EL على استدعاء دوال Java في الشروط. كتابة ${products.size() gt 0} تعمل، لكن ${not empty products} هو الأسلوب الاصطلاحي في JSTL ويتعامل مع null بأمان — فـsize() على مرجع null تُلقي NullPointerException.

لماذا تنتمي هذه الوسوم إلى كل ملف JSP

تُنفّذ هذه الوسوم الأربعة مبدأ العرض الخالي من المنطق: قرارات العمل (ما البيانات التي تعرضها، كيف تحسب المجموع) تنتمي إلى طبقة Servlet أو الخدمة؛ مهمة JSP الوحيدة هي عرض حالة معطاة. تعبّر c:if/c:choose عن قرارات العرض مثل "أظهر رسالة الحالة الفارغة" أو "أضف فئة CSS عندما يكون هذا أول عنصر". يعرض c:forEach المجموعات التي جهّزها المتحكم. يُنشئ c:set أسماء بديلة محلية للحفاظ على قابلية قراءة القالب. معاً تتيح هذه الوسوم لمصمم تعديل HTML دون لمس أي Java، وتتيح لمطوّر اختبار وحدة كل المنطق قبل كتابة العرض حتى.

الخلاصة

  • c:set — تعريف متغير ذي نطاق أو اسم بديل؛ يُلغي تعريفات الـ Scriptlet <% %>.
  • c:if — شرط أحادي الفرع؛ استخدمه حين تحتاج فقط حارساً إيجابياً.
  • c:choose / c:when / c:otherwise — شرط متعدد الفروع؛ البديل النظيف لأزواج c:if المتتالية.
  • c:forEach — التكرار على أي مجموعة أو مصفوفة أو Map أو مدى صحيح؛ استخدم varStatus للفهرس والعداد وعلامتَي first/last.

في الدرس القادم ستوسّع JSTL بمكتبتَي وسوم Formatting وFunctions للتعامل مع التواريخ والأرقام ومعالجة النصوص — للإبقاء على منطق التنسيق خارج كود Java أيضاً.