هش‌شمار


Hash Collision

محمدرضا علاقه‌ی زیادی به بهینه بودن برنامه‌ها دارد. او که در زمینه‌ی الگوریتم بسیار خفن است، می‌داند که استفاده از hash برای نگه‌داری داده‌ها در برخی مواقع نه تنها کمکی به افزایش پرفورمنس نمی‌کند، بلکه پرفورمنس برنامه را کاهش می‌دهد. نیما که حرف محمدرضا را قبول ندارد، از او درخواست کدی کرده است تا میزان hash collision ها را ببیند. محمدرضا نیز این کار را به شما محول کرده است.

در این سؤال، یک HashSet یا HashMap به شما داده می‌شود. شما باید تعداد یکتای hash code های مقادیر موجود در HashSet یا کلیدهای موجود در HashMap را محاسبه کرده و برگردانید.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های پروژه به‌صورت زیر است:

hash-collision-checker
├── HashCollisionChecker.java
└── HashCollisionCheckerSampleTest.java
Plain text

کلاس HashCollisionChecker🔗

این کلاس شامل دو متد زیر است که باید آن‌ها را پیاده‌سازی کنید:

محاسبه‌ی تعداد یکتای hash code های موجود در یک HashSet🔗

public static <T> int countOfUniqueHashCodes(HashSet<T> set)
Java

این متد را طوری پیاده‌سازی کنید که با دریافت یک HashSet، تعداد یکتای hash code های مقادیر موجود در آن را برگرداند.

محاسبه‌ی تعداد یکتای hash code های موجود در یک HashSet🔗

public static <K, V> int countOfUniqueHashCodes(HashMap<K, V> map)
Java

این متد را طوری پیاده‌سازی کنید که با دریافت یک HashMap، تعداد یکتای hash code های کلیدهای موجود در آن را برگرداند.

مثال🔗

با اجرای متد main موجود در کلاس HashCollisionChecker:

public static void main(String[] args) {
    HashSet<String> set = new HashSet<>();
    set.add("c#c#c#c#c#c#bBc#c#c#c#bBc#");
    set.add("abcd");
    set.add("c#c#c#c#c#c#bBc#c#c#c#c#aa");
    set.add("1234");
    set.add("c#c#c#c#c#c#bBc#c#c#c#c#bB");
    System.out.println(countOfUniqueHashCodes(set)); // 3

    HashMap<String, Integer> map = new HashMap<>();
    map.put("c#c#c#c#c#c#c#aaaaaaaabBbB", 14);
    map.put("c#c#c#c#c#c#c#aaaaaaaac#c#", 12);
    map.put("c#c#c#c#c#c#c#aaaaaaaac#cc", 16);
    System.out.println(countOfUniqueHashCodes(map)); // 2
}
Java

خروجی باید به‌صورت زیر باشد:

3
2
Plain text

نکات🔗

  • برای دریافت hash code آبجکت‌ها، می‌توانید از متد hashCode استفاده کنید.
  • تست‌های نمونه‌ی سؤال در کلاس HashCollisionCheckerSampleTest موجود هستند. با افزودن JUnit به classpath پروژه، می‌توانید آن‌ها را اجرا کنید.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی متدها، فایل HashCollisionChecker.java را آپلود کنید.

زمان اجرا


سلیب که در خلوت خود در حال تفکری عمیق درباره اردر‌های مختلف بود، به این فکر فرو رفت که متدهایی که پیاده‌سازی کرده چقدر طول می‌کشند و چقدر بهینه کد می‌زند. برای همین سراغ پیاده‌سازی متدی برای سنجش زمان موردنیاز برای اجرای هر متد خود رفت. در ادامه به توضیح این متد می‌پردازیم.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

ساختار فایل‌های پروژه به‌صورت زیر است:

execution-time
└── ExecutionTimeCalculator.java
Plain text

شما باید متد measureExecutionTime موجود در کلاس ExecutionTime را مطابق با خواست سؤال پیاده‌سازی کنید. امضای این متد به‌صورت زیر است:

extensionFromNameExecutionTimeCalculator.java
import java.math.BigDecimal;

public class ExecutionTimeCalculator {
    public static long functionToCall() {
        long returnValue = 0;
        for (long i = 1; i < 1000L * 1000 * 1000 * 10; i++) {
            returnValue += i;
        }
        return returnValue;
    }

    public static BigDecimal measureExecutionTime(Runnable function) {
        // TODO: Implement
        return null;
    }

    public static void main(String[] args) {
        System.out.println(measureExecutionTime(ExecutionTimeCalculator::functionToCall));
    }
}
Java

این متد باید Runnableای که به آن ورودی داده می‌شود را اجرا کند و زمان مورد نیاز برای اجرای آن را با واحد ثانیه در متغیری از جنس BigDecimal دقیقاً با ۵ رقم پس از اعشار برگرداند. برای کاهش ارقام اعشار باید عدد را از پنجمین رقم در ممیز گرد کنید (برای این کار، می‌توانید از RoundingMode.HALF_UP استفاده کنید).

نکات🔗

  • توجه داشته باشید که خروجی شما باید دقیقاً شامل ۵ رقم پس از اعشار باشد.
  • عدد محاسبه‌شده را باید گرد کنید.
  • زمان محاسبه‌شده باید با واحد ثانیه باشد.
  • متد run در شیء Runnable متد را اجرا می‌کند.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی موارد خواسته‌شده، فایل ExecutionTimeCalculator.java را آپلود کنید.

کَش‌مَش


Cache

طبق آماری که گوگل در سال ۲۰۱۶ منتشر کرد، ٪۵۳ از بازدید‌کنندگان، یک وب‌سایت را در صورتی که لود آن بیش از ۳ ثانیه طول بکشد رها می‌کنند! این آمار، ابوالفضل را به این فکر فرو برده که سرعت لود یک وب‌سایت به چه اندازه روی حس رضایت کاربران آن وب‌سایت تأثیر دارد. او که حالا هشتگ #SpeedMatters را در شبکه‌های اجتماعی ترند کرده، به این فکر افتاده که سرعت لود برنامه‌اش را با استفاده از cache بالا ببرد. او برای این کار نیاز به یک دولوپر دیگر دارد و از شما کمک خواسته است.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های پروژه به‌صورت زیر است:

cache
├── models
│   ├── CacheData.java
│   ├── Comment.java
│   ├── ExportCacheDTO.java
│   └── Photo.java
├── services
│   ├── Cache.java
│   └── CacheItem.java
└── Application.java
Plain text

شما باید کلاس Cache را طوری کامل کنید تا خواسته‌هایی را که در ادامه مطرح می‌شود را برآورده کند.

ساختار اولیه‌ی این کلاس به‌صورت زیر است:

extensionFromNameservices/Cache.java
package services;

import java.util.ArrayList;
import java.util.List;

public class Cache {
    private static final int TTL = 2;

    private List<CacheItem> data = new ArrayList<>();

    public List<CacheItem> getAll() {
        return data;
    }
}
Java

همان‌طور که مشخص است، cache ما قرار است آیتم‌های مختلفی را توسط کلاس CacheItem درون خود نگه دارد. این آیتم‌ها فعلاً شامل دو کلاس Photo و Comment می‌شوند. هم‌چنین این آیتم‌ها زمان مجازی دارند که توسط پراپرتی TTL (واحد روز) مشخص می‌شود. در اولین گام باید کلاس CacheItem را کامل کنید.

اینترفیس CacheData🔗

این اینترفیس، بیانگر داده‌های موجود در cache است. متد زیر در این اینترفیس تعریف شده است:

long getID();
Java

کلاس‌های Comment و Photo این اینترفیس را پیاده‌سازی می‌کنند.

کلاس CacheItem🔗

این کلاس به‌صورت generic بوده و مشخص‌کننده‌ی آیتم‌هایی است که درون cache نگه داشته می‌شوند. این کلاس باید شامل دو پراپرتی data از نوع T (به‌صورت generic) و createdAt از نوع LocalDateTime باشد.

از پراپرتی createdAt برای بررسی مجاز بودن یک آیتم cache استفاده می‌شود. زمان مجاز زنده بودن یک آیتم درون cache طبق مقدار پراپرتی TTL در کلاس Cache برابر با ۲ روز است.

کلاس Cache🔗

همان‌طور که گفته شد، مهم‌ترین بخش برنامه، این کلاس است. شما باید متد‌هایی را که در ادامه ذکر می‌شود را در این کلاس پیاده‌سازی کنید:

  • متد add: این متد که ورودی آن CacheItem است، باید بتواند یک آیتم را به cache اضافه کند. دقت کنید که آیتمی با دیتای تکراری نباید در cache وجود داشته باشد. در این‌صورت، آیتم قبلی باید از لیست آیتم‌ها حذف شده و آیتم جدید درج شود.
  • متد remove: این متد که ورودی آن CacheItem است، باید بتواند یک آیتم را از cache حذف کند.

دو متد بالا را باید بتوان به روش method chaining صدا زد. مثال:

cache.add(item1).remove(item1).add(item2);
Java
  • متد clear: این متد تمام آیتم‌های درون cache را حذف می‌کند.
  • متد getAll: این متد تمام آیتم‌های مجاز درون cache را در قالب لیستی از CacheItemها برمی‌گرداند.
  • متد findByID: این متد یک پارامتر ورودی از نوع long دارد و باید لیستی از CacheItemهای مجاز را در صورتی که پراپرتی ID آیتم درون cache با پارامتر ورودی برابر بود برگرداند.
  • متد filter: این متد دو امضا به‌صورت زیر دارد:
    • اولین امضای این متد دو پارامتر ورودی دارد. اولین پارامتر، نام پراپرتی از نوع String است. دومین پارامتر، یک عبارت باقاعده (Regex) به‌صورت String است. در این حالت، تضمین می‌شود که پراپرتی‌ای با نام ذکرشده در همه‌ی داده‌های موجود در cache تعریف شده است.
    • دومین امضای این متد نیز مانند امضای اول است، فقط پارامتر نوع کلاس به‌عنوان اولین پارامتر به این متد اضافه شده و مشخص‌کننده‌ی نوع کلاسی است که باید جست‌وجو فقط در داده‌های از نوع کلاس واردشده صورت گیرد. در این حالت، تضمین می‌شود که پراپرتی‌ای با نام ذکرشده در کلاس ذکرشده تعریف شده است.

کاری که این متد انجام می‌دهد آن است که باید در پراپرتی گفته‌شده، عبارت باقاعده را جست‌وجو کند و لیستی از CacheItemهای مجاز را برگرداند. به عنوان مثال فرض کنید دو آیتم زیر درون cache وجود دارند:

Photo{ID=1, name='tree.png', path='/production', size=10.0}
Photo{ID=2, name='weather.jpg', path='/production', size=10.0}
Plain text

حال متد filter را به‌صورت زیر صدا می‌ز‌نیم و می‌خواهیم عکس‌های با فرمت png را فیلتر کنیم:

extensionFromNameApplication.java
cache.filter(Photo.class, "name", ".png")
Java

خروجی باید به‌صورت زیر باشد:

Photo{ID=1, name='tree.png', path='/production', size=10.0}
Plain text
  • متد export: از این متد نیز قرار است برای به‌دست آوردن آماری در رابطه با آیتم‌های درون cache استفاده کنیم. خروجی این متد لیستی از ExportCacheDTOها است. مثالی از خروجی این متد:
[
ExportCacheDTO{name='Photo', size=2},
ExportCacheDTO{name='Comment', size=3}
]
Plain text

این خروجی به این معنی است که ۲ آیتم از نوع Photo و ۳ آیتم از نوع Comment درون cache وجود دارد.

نکات🔗

  • آیتمی با دیتای تکراری نباید در cache وجود داشته باشد.
  • اگر قرار است در بعضی از متد‌ها آیتمی را برگردانید، آن آیتم حتماً باید مجاز باشد (منقضی نشده باشد).
  • ترتیب برگرداندن آیتم‌ها باید مطابق با ترتیب اضافه شدن آن‌ها به cache باشد.
  • شما تنها مجاز به اعمال تغییرات در فایل‌های services/Cache.java و services/CacheItem.java هستید.
  • تست‌های نمونه‌ی برنامه در کلاس CacheSampleTest موجود هستند. می‌توانید آن‌ها را پس از افزودن JUnit به classpath در سیستم خود اجرا کنید.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی موارد خواسته‌شده، پوشه‌ی services را زیپ کرده و آپلود کنید (خود پوشه نیز در فایل زیپ موجود باشد).

جام‌کاوی


معدن جام

علی در شبک‌های اجتماعی، کانال‌هایی برای فوتبالی‌ها دارد. او می‌خواهد برنامه‌ی زمان‌بندی بازی‌های فوتبال پیش رو را در کانال‌هایش به‌صورت خودکار ارسال کند. همچنین، می‌خواهد پیش از شروع هر بازی، یک پیام یادآوری به‌صورت خودکار بفرستد. برای این کار، او باید اطلاعات بازی‌ها را داشته باشد. از شما می‌خواهیم برنامه‌ای بنویسید که این اطلاعات را از صفحات وب جمع‌آوری کند.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های پروژه به‌صورت زیر است:

world-cup-crawler
├── www
│   ├── ahmad-bin-ali-stadium.html
│   ├── al-bayt-stadium.html
│   ├── al-janoub-stadium.html
│   ├── al-thumama-stadium.html
│   ├── education-city-stadium.html
│   ├── index.html
│   ├── khalifa-international-stadium.html
│   ├── lusail-iconic-stadium.html
│   └── stadium-974.html
├── Country.java
├── HttpHandler.java
├── Match.java
├── Server.java
├── Stadium.java
├── Team.java
├── WorldCupCrawler.java
└── WorldCupCrawlerSampleTest.java
Plain text

کلاس Country🔗

هر نمونه از این کلاس، بیانگر یک کشور است. هر Country دارای یک name از نوع String بوده که بیانگر نام کشور است. این کلاس از قبل به‌طور کامل پیاده‌سازی شده است.

کلاس Stadium🔗

هر نمونه از این کلاس، بیانگر یک استادیوم است. هر Stadium دارای ویژگی‌های زیر است:

  • نام (name) از نوع String
  • کشور (country) از نوع Country
  • ظرفیت (capacity) از نوع int

این کلاس از قبل به‌طور کامل پیاده‌سازی شده است.

کلاس Team🔗

هر نمونه از این کلاس، بیانگر یک تیم است. هر Team دارای یک name از نوع String بوده که بیانگر نام کشور است. این کلاس از قبل به‌طور کامل پیاده‌سازی شده است.

کلاس Match🔗

هر نمونه از این کلاس، بیانگر یک مسابقه است. هر Match دارای ویژگی‌های زیر است:

  • زمان بازی (time) از نوع Date
  • گروه (group) از نوع String
  • تیم میزبان (team1) از نوع Team
  • تیم میزبان (team2) از نوع Team
  • استادیوم (stadium) از نوع Stadium

این کلاس از قبل به‌طور کامل پیاده‌سازی شده است.

کلاس WorldCupCrawler🔗

این کلاس، وظیفه‌ی استخراج اطلاعات بازی‌ها از صفحات وب را برعهده دارد. عملیات استخراج داده‌ها باید تا حد امکان با استفاده از چند thread انجام شود تا سرعت بیشتری داشته باشد. این کلاس در کانستراکتور خود یک آبجمت از نوع URL دریافت می‌کند که بیانگر آدرس صفحه‌ی لیست بازی‌ها است. نمونه‌ای از صفحه‌ی لیست بازی‌ها در فایل www/index.html موجود است.

  • متد استاتیک getStadium این کلاس را طوری پیاده‌سازی کنید که با دریافت یک URL، اطلاعات یک استادیوم را از URL داده‌شده استخراج کرده و در قالب یک آبجکت از نوع Stadium برگرداند. نمونه‌ای از نحوه‌ی فراخوانی این متد:
WorldCupCrawler.getStadium(new URL("http://localhost/al-bayt-stadium.html"))
Java
  • متد getMatches را طوری پیاده‌سازی کنید که با ارسال درخواست به آدرس واردشده در کانستراکتور، اطلاعات بازی‌ها را در قالب یک List از Matchها برگرداند.

نکته: تست‌های نمونه‌ی سؤال در کلاس WorldCupCrawlerSampleTest موجود هستند. با افزودن JUnit به classpath پروژه، می‌توانید آن‌ها را اجرا کنید. کلاس‌های Server و HtttpHandler نیز جزو کلاس‌های کمکی برای تست هستند.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی موارد خواسته‌شده، فایل WorldCupCrawler.java را آپلود کنید.

سرویس احراز هویت پیامکی


سلیب که از امنیت کوئرا به ستوه آمده بود، با نیما (اسطوره‌ی امنیت در سطح بین‌المللی) در این‌باره صحبتی را شروع کرد. نیما به این نتیجه رسید که کوئرا نیاز به یک اقدام امنیتی در ورود به حساب کاربری دارد. پیشنهاد او تأیید از طریق SMS بود و تأکید اکیدی بر زمان انقضای آن‌ها داشت. به نظر او هر کد تأیید باید فقط برای ۱۰ ثانیه معتبر می‌بودند و پس از گذشت بیش از ۱۰ ثانیه، کد دیگر مورد قبول نباشد. سلیب از آن‌سو تاکید بر ماندگاری دیتا‌ها داشت. به نظر او نیاز بود تا تمامی اطلاعات را به فرمت زیر در یک فایل CSV نگه‌داری کند:

extensionFromNamesample-data.csv
Phone,Code,Expiration
CSV

در ستون اول شماره مورد نظر، ستون دوم کد ساخته شده برای شماره و در ستون سوم اعتبار کد را نمایش می‌دهد، در ادامه به بیان جزئیات بیشتر سؤال می‌پردازیم.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

ساختار فایل‌های پروژه به‌صورت زیر است:

SMSAuthentication
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── org
│   │           └── quera
│   │               └── smsauthentication
│   │                   └── SMSAuthentication.java
│   └── test
│       └── java
│           └── org
│               └── quera
│                   └── smsauthentication
│                       └── SMSAuthenticationSampleTest.java
└── test.csv
Plain text

شما باید متدهای generateCode، sendAuthenticationSMS، loadData، verifyCode موجود در کلاس SMSAuthentication را مطابق با خواسته‌های سؤال پیاده‌سازی کنید. نمای کلی فایل مورد نظر به شکل زیر است:

extensionFromNameSMSAuthentication.java
package org.quera.smsauthentication;

import ...

public class SMSAuthentication {
    public static String generateCode() {
        // TODO: Implement
    }

    public static String sendAuthenticationSMS(String phone, String path) {
        // TODO: Implement
    }

    public static List<String[]> loadData(String path) {
        // TODO: Implement
    }

    public static boolean verifyCode(String code, String phone, String path) {
        // TODO: Implement
    }

    public static void main(String[] args) {
        System.out.println(sendAuthenticationSMS("09381358888", "test.csv"));
        Scanner sc = new Scanner(System.in);
        String code = sc.next();
        System.out.println(verifyCode(code,"09381358888","test.csv"));
    }
}
Java

متد generateCode🔗

این متد باید یک رشته تصادفی به طول ۵ بسازد و مقدار آن را برگرداند. در این رشته می‌توانید از فقط متشکل از ارقام انگلیسی استفاده کنید. نمونه‌ی اجرای متد در زیر آمده است:

extensionFromNameTerminal
>>> generateCode()
40142
>>> generateCode()
18272
>>> generateCode()
00168
Java

متد sendAuthenticationSMS🔗

این متد یک رشته به عنوان شماره و رشته‌ای دیگر به عنوان آدرس فایلی که باید اطلاعات را در آن ذخیره کند، به عنوان ورودی دریافت می‌کند. خروجی این متد باید رشته‌ای به فرمت JSON باشد. در خروجی JSON مورد نظر باید کلید و مقدارهای زیر را داشته باشد:

  • کلید phone برابر با مقدار شماره مورد نظر خواهد بود.
  • کلید code برابر با کد ساخته شده برای شماره مورد نظر خواهد بود.
  • کلید exp برابر با زمانی که کد مورد نظر منقضی می‌شود است، زمان در این سؤال به فرمت "yyyy/MM/dd-HH:mm:ss" استفاده می‌شود.

این متد باید این محتوا را درون فایل CSV موجود در آدرس path ذخیره کند. خروجی و رفتار خواسته شده در بخش مثال با جزئیات بیشتری نمایش داده شده است.

مثال🔗

extensionFromNameTerminal
>>> sendAuthenticationSMS("09381354444", "test.csv")
{"phone":"09381354444","code":"06505","exp":"2022/10/04-18:02:15"}
>>> sendAuthenticationSMS("09381354443", "test.csv")
{"phone":"09381354443","code":"26052","exp":"2022/10/04-18:07:39"}
>>> sendAuthenticationSMS("09381354444", "test.csv") // duplicate
{"phone":"09381354444","code":"00300","exp":"2022/10/04-19:02:48"}
Java

محتوای چند خط آخر فایل test.csv پس از اجرای بالا برابر با زیر خواهد بود:

extensionFromNametest.csv
Phone,Code,Expiration
...
09381354444,06505,2022/10/04-18:02:15
09381354443,26052,2022/10/04-18:07:39
09381354444,00300,2022/10/04-19:02:48
CSV

متد loadData🔗

این متد یک رشته به عنوان آدرس فایلی که باید اطلاعات آن را بخواند، به عنوان ورودی دریافت می‌کند. خروجی این متد لیستی از آرایه‌ای از Stringها خواهد بود که برابر با مقادیر موجود در فایل CSV ورودی متد است.

مثال🔗

برای مثال فرض کنید محتوای فایل CSV به‌صورت زیر باشد:

extensionFromNametest.csv
Phone,Code,Expiration
09381354444,06505,2022/10/04-18:02:15
09381354443,26052,2022/10/04-18:07:39
09381354444,00300,2022/10/04-19:02:48
CSV

اگر متد را به‌صورت زیر فراخوانی کنیم

extensionFromNamesnippet
List<String[]> allData = loadData("test.csv");
Java

و بخواهیم خروجی متد را به‌صورت زیر چاپ کنیم:

extensionFromNamesnippet
for(String[] data : allData) {
    for(String attr : data) {
        System.out.print(attr + " ");
    }
    System.out.println();
}
Java

خروجی به‌صورت زیر خواهد بود

09381354444 06505 2022/10/04-18:02:15
09381354443 26052 2022/10/04-18:07:39
09381354444 00300 2022/10/04-19:02:48
Plain text

دقت کنید که خط اول (یا همان Phone,Code,Expiration) در خروجی متد وجود ندارد.

متد verifyCode🔗

این متد یک رشته به عنوان کد، یک رشته به عنوان شماره و رشته‌ای دیگر به عنوان آدرس فایلی که باید اطلاعات را از آن بخواند، به عنوان ورودی دریافت می‌کند. خروجی این متد یک boolean است که در صورتی که کد با شماره مورد نظر تطابق داشت و کد منقضی نشده بود، باید مقدار true برگرداند و در غیر این صورت مقدار false برگرداند.

نکات🔗

  • شما در این سؤال مجاز به تغییر فایل pom.xml نیستید.
  • برای کار با JSON، تنها مجاز به استفاده از کتابخانه opencsv هستید.
  • برای کار با CSV ، تنها مجاز به استفاده از کتابخانه jackson-databind هستید.
  • به جز کتابخانه‌های موجود در pom.xml، تنها مجاز به استفاده از کتابخانه‌های استاندارد جاوا هستید.
  • هر کد تنها ده ثانیه زمان دارد.
  • دقت کنید که تاریخ و زمان انقضای کد را باید دقیقا برابر با ۱۰ ثانیه پس از تولید کد (یا فراخوانی متد sendAuthenticationSMS) قرار دهید.
  • شما مجاز به عوض کردن فرمت ذخیره‌سازی اطلاعات در فایل CSV نیستید و باید دقیقا برابر با فرمت خواسته شده، اطلاعات را ذخیره سازی کنید.
  • داوری این سؤال، بیشتر از باقی مسائل طول می‌کشد.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی موارد خواسته‌شده، فایل تمامی پروژه را زیپ کنید و آن را آپلود کنید. فایل زیپ شما باید به‌صورتی باشد که پس از خروج از حالت فشرده، پوشه‌ی src مشاهده شود.

پارسر لاگ Nginx


Nginx

اخیراً وب‌سایت شخصی نیما مورد حملات مشکوک قرار گرفته است. او در حال بررسی لاگ‌های وب‌سرور سایتش است تا دریابد این حملات از کدام سمت بوده‌اند، اما برای این کار از یک ویرایشگر متنی ساده استفاده می‌کند. او می‌خواهد بتواند روی لاگ‌ها فیلتر اعمال کند تا جست‌وجوی هدفمندی داشته باشد. از شما می‌خواهیم چنین برنامه‌ای را برای او پیاده‌سازی کنید.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های پروژه به‌صورت زیر است:

nginx-log-parser
├── exceptions
│   ├── LineDoesNotMatchException.java
│   ├── PropertyNotFoundException.java
│   └── TransformerNotFoundException.java
├── Condition.java
├── Log.java
├── LogList.java
├── LogParser.java
├── LogParserSampleTest.java
└── LogPropertyTransformerRepository.java
Plain text

بسته‌ی exceptions🔗

این بسته شامل استثناهای موردنیاز برنامه است. همه‌ی کلاس‌های موجود در این بسته از کلاس RuntimeException ارث‌بری می‌کنند.

کلاس Log🔗

هر نمونه از این کلاس بیانگر لاگ یک درخواست است. هر لاگ شامل یک یا چند ویژگی است. این کلاس از قبل به‌طور کامل پیاده‌سازی شده و شامل متدهای زیر است:

  • متد setProperty(String name, String value): این متد با دریافت نام و مقدار یک ویژگی، آن ویژگی را به ویژگی‌های لاگ اضافه می‌کند.
  • متد getProperty(String name): این متد با دریافت نام یک ویژگی، مقدار آن ویژگی را برمی‌گرداند. در صورتی که ویژگی‌ای با نام واردشده موجود نباشد، یک استثنا از نوع PropertyNotFoundException پرتاب می‌شود.

کلاس LogPropertyTransformerRepository🔗

بعضی از ویژگی‌های لاگ‌ها (مانند کد وضعیت پاسخ و زمان ارسال درخواست) ماهیت عددی دارند. این کلاس، مجموعه‌ای از Function<String, Long> را در خود نگه‌داری می‌کند تا با استفاده از آن‌ها، بتوان مقدار عددی معادل بعضی از ویژگی‌های لاگ‌ها را دریافت کرد. این کلاس از قبل به‌طور کامل پیاده‌سازی شده و شامل متدهای زیر است:

  • متد addTransformer(String propertyName, Function<String, Long> transformer): این متد با دریافت نام یک ویژگی و یک تابع برای تبدیل مقدار رشته‌ای ویژگی به مقدار عددی، آن تابع را در Map موجود در کلاس ذخیره می‌کند.
  • متد getTransformer(String propertyName): این متد با دریافت نام یک ویژگی، تابع مبدل مقدار آن ویژگی به مقدار عددی را برمی‌گرداند. در صورتی که تابعی برای ویژگی موردنظر تعریف نشده باشد، یک استثنا از نوع TransformerNotFoundException پرتاب می‌شود.

کلاس Condition🔗

هر نمونه از این کلاس، بیانگر یک شرط (ساده یا مرکب) برای فیلتر کردن لاگ‌ها است. پراپرتی‌های موردنیاز خود را در این کلاس تعریف کرده و متدهای زیر را در آن پیاده‌سازی کنید:

  • متد استاتیک equal(String propertyName, String value) را طوری پیاده‌سازی کنید که با دریافت نام و مقدار یک ویژگی، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط معادل بودن ویژگی با مقدار موردنظر در آن موجود باشد.
  • متد استاتیک notEqual(String propertyName, String value) را طوری پیاده‌سازی کنید که با دریافت نام و مقدار یک ویژگی، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط معادل نبودن ویژگی با مقدار موردنظر در آن موجود باشد.
  • متد استاتیک in(String propertyName, String... values) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و تعدادی مقدار، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط معادل بودن ویژگی با حداقل یکی از مقادیر واردشده در آن موجود باشد.
  • متد استاتیک in(String propertyName, String... values) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و تعدادی مقدار، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط معادل نبودن ویژگی با هیچ یک از مقادیر واردشده در آن موجود باشد.
  • متد استاتیک lessThan(String propertyName, Long value) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و یک عدد، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط کوچک‌تر بودن مقدار عددی ویژگی از عدد واردشده در آن موجود باشد. برای پیاده‌سازی این متد، از کلاس LogPropertyTransformerRepository استفاده کنید.
  • متد استاتیک lessOrEqual(String propertyName, Long value) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و یک عدد، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط کوچک‌تر یا مساوی بودن مقدار عددی ویژگی از عدد واردشده در آن موجود باشد. برای پیاده‌سازی این متد، از کلاس LogPropertyTransformerRepository استفاده کنید.
  • متد استاتیک greaterThan(String propertyName, Long value) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و یک عدد، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط بزرگ‌تر بودن مقدار عددی ویژگی از عدد واردشده در آن موجود باشد. برای پیاده‌سازی این متد، از کلاس LogPropertyTransformerRepository استفاده کنید.
  • متد استاتیک greaterOrEqual(String propertyName, Long value) را طوری پیاده‌سازی کنید که با دریافت نام یک ویژگی و یک عدد، یک نمونه از کلاس Condition برگرداند که اطلاعات مربوط به شرط بزرگ‌تر یا مساوی بودن مقدار عددی ویژگی از عدد واردشده در آن موجود باشد. برای پیاده‌سازی این متد، از کلاس LogPropertyTransformerRepository استفاده کنید.
  • متد and(Condition condition) را طوری پیاده‌سازی کنید که با دریافت یک شرط، یک نمونه از کلاس Condition برگرداند که شرط آن برابر با AND منطقی شرط فعلی و شرط واردشده باشد.
  • متد or(Condition condition) را طوری پیاده‌سازی کنید که با دریافت یک شرط، یک نمونه از کلاس Condition برگرداند که شرط آن برابر با OR منطقی شرط فعلی و شرط واردشده باشد.
  • متد استاتیک not(Condition condition) را طوری پیاده‌سازی کنید که با دریافت یک شرط، یک نمونه از کلاس Condition برگرداند که شرط آن برابر با NOT منطقی شرط فعلی و شرط واردشده باشد.
  • متد test(Log log) را طوری پیاده‌سازی کنید که با دریافت یک لاگ، در صورتی که لاگ در شرایط فعلی صدق کند، مقدار true و در غیر این‌صورت، مقدار false را برگرداند.

نکته: متدهای and، or و not را باید به‌گونه‌ای پیاده‌سازی کنید که نمونه‌های جدیدی از کلاس Condition ایجاد شوند. در واقع، نمونه‌های کلاس Condition باید immutable باشند.

کلاس LogList🔗

این کلاس، یک List از لاگ‌ها را در خود نگه‌داری می‌کند.

  • متد all این کلاس را طوری پیاده‌سازی کنید که یک LogList شامل یک List جدید که لاگ‌های موجود در آبجکت فعلی در آن قرار گرفته‌اند ایجاد کرده و برگرداند.
  • متد where این کلاس را طوری پیاده‌سازی کنید که با دریافت یک Condition، یک LogList شامل یک List جدید که لاگ‌های موجود در آبجکت فعلی در آن قرار گرفته‌اند و در شرط واردشده صدق می‌کنند ایجاد کرده و برگرداند. ترتیب لاگ‌های موجود در لیست را نباید تغییر دهید.

کلاس LogParser🔗

وظیفه‌ی این کلاس، پارس کردن رشته‌ی لاگ‌ها است. لاگ‌ها یک schema مشخص دارند که در قالب یک رشته به کانستراکتور این کلاس داده می‌شود. schema شامل تعدادی متغیر نیز هست که با $ آغاز می‌شوند. مثالی از یک schema:

$remote_addr - $remote_user [$time_local] "$request" $status $bytes_sent "$http_referer" "$http_user_agent"
Plain text

در این‌صورت، لاگ‌های پارس‌شده باید شامل ویژگی‌های remote_addr، remote_user و... باشند.

نمونه‌ای از یک لاگ که طبق فرمت schema بالا است:

127.0.0.1 - - [07/Jul/2018:17:37:28 +0200] "GET /7d32ce87648a4050faca.hot-update.json HTTP/1.1" 200 43 "http://test.local/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0"
Plain text
  • متد parseLog(String line) این کلاس را طوری پیاده‌سازی کنید که با دریافت یک رشته‌ی یک‌خطی، یک Log متناظر با لاگ واردشده برگرداند. در صورتی که لاگ واردشده با schema داده‌شده مطابقت نداشت، یک استثنا از نوع LineDoesNotMatchException پرتاب کنید. ترتیب ویژگی‌های Log خروجی باید مطابق ترتیب متغیرهای موجود در schema باشد.
  • متد parseLogs(String logs) را طوری پیاده‌سازی کنید که با دریافت یک رشته‌ی یک یا چند خطی، یک LogList متناظر با لاگ‌های واردشده برگرداند. برای پیاده‌سازی این متد، از متد parseLog(String line) استفاده کنید.
  • متد parseLogs(File file) را طوری پیاده‌سازی کنید که با دریافت یک فایل، یک LogList متناظر با لاگ‌های موجود در آن برگرداند. برای پیاده‌سازی این متد، از متد parseLogs(String logs) استفاده کنید.

مثال🔗

با اجرای متد main موجود در کلاس LogParser:

public static void main(String[] args) {
    LogPropertyTransformerRepository.addTransformer("status", Long::parseLong);
    String schema = "$remote_addr - $remote_user [$time_local] \"$request\" $status $bytes_sent \"$http_referer\" \"$http_user_agent\"";
    LogParser parser = new LogParser(schema);
    LogList logs = parser.parseLogs("127.0.0.1 - - [07/Jul/2018:17:37:28 +0200] \"GET /7d32ce87648a4050faca.hot-update.json HTTP/1.1\" 200 43 \"http://test.local/\" \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0\"");
    System.out.println(logs.where(Condition.greaterOrEqual("status", 200L)));
}
Java

خروجی باید به‌صورت زیر باشد:

[Log[remote_addr=127.0.0.1, remote_user=-, time_local=07/Jul/2018:17:37:28 +0200, request=GET /7d32ce87648a4050faca.hot-update.json HTTP/1.1, status=200, bytes_sent=43, http_referer=http://test.local/, http_user_agent=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0]]
Plain text

نکته: تست‌های نمونه‌ی سؤال در کلاس LogParserSampleTest موجود هستند. با افزودن JUnit به classpath پروژه، می‌توانید آن‌ها را اجرا کنید.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی موارد خواسته‌شده، یک فایل زیپ آپلود کنید که وقتی آن را باز می‌کنیم، با فایل‌های زیر مواجه شویم:

Condition.java
LogList.java
LogParser.java
Plain text