|
| 1 | +# Домашно 2 |
| 2 | + |
| 3 | +## Space Scanner :rocket: |
| 4 | + |
| 5 | +`Краен срок: 22.12.2025, 23:59` |
| 6 | + |
| 7 | +### Условие |
| 8 | + |
| 9 | +Ще създадем приложение за извличане на статистически резултати за мисиите в Космоса от 1957г. насам, заедно с ракетите, |
| 10 | +използвани за съответните мисии. |
| 11 | + |
| 12 | +Ще използваме data set от [kaggle](https://www.kaggle.com). Данните за мисиите са налични в CSV файлa [all-missions-from-1957.csv](./resources/all-missions-from-1957.csv). Данните за ракетите са налични в CSV файла [all-rockets-from-1957.csv](./resources/all-rockets-from-1957.csv). |
| 13 | + |
| 14 | +Имайте предвид, че е възможно в real-life data set да има непълни записи, т.е. да липсва информация за дадена колона. |
| 15 | +Такива ще моделираме с `Optional`. Обърнете внимание, че символът за запетая участва както като разделител между колоните, така и като част от данните в самите тях. |
| 16 | + |
| 17 | +### Задължителни интерфейси и класове |
| 18 | + |
| 19 | +В пакета `bg.sofia.uni.fmi.mjt.space` създайте клас `MJTSpaceScanner`, който има конструктор, приемащ мисиите, под формата на `Reader`, ракетите, отново под формата на `Reader`, както и частен ключ, който се използва за криптиране и декриптиране на конфиденциална информация, използвайки **Rijndael** (или **AES**) алгоритъма. |
| 20 | + |
| 21 | +```java |
| 22 | +public MJTSpaceScanner(Reader missionsReader, Reader rocketsReader, SecretKey secretKey) |
| 23 | +``` |
| 24 | + |
| 25 | +Класът `MJTSpaceScanner` имплементира интерфейса `SpaceScannerAPI`: |
| 26 | + |
| 27 | +```java |
| 28 | +package bg.sofia.uni.fmi.mjt.space; |
| 29 | + |
| 30 | +import bg.sofia.uni.fmi.mjt.space.mission.Mission; |
| 31 | +import bg.sofia.uni.fmi.mjt.space.mission.MissionStatus; |
| 32 | +import bg.sofia.uni.fmi.mjt.space.rocket.Rocket; |
| 33 | +import bg.sofia.uni.fmi.mjt.space.rocket.RocketStatus; |
| 34 | + |
| 35 | +import java.io.OutputStream; |
| 36 | +import java.time.LocalDate; |
| 37 | +import java.util.Collection; |
| 38 | +import java.util.List; |
| 39 | +import java.util.Map; |
| 40 | +import java.util.Optional; |
| 41 | + |
| 42 | +public interface SpaceScannerAPI { |
| 43 | + /** |
| 44 | + * Returns all missions in the dataset. |
| 45 | + * If there are no missions, return an empty collection. |
| 46 | + */ |
| 47 | + Collection<Mission> getAllMissions(); |
| 48 | + |
| 49 | + /** |
| 50 | + * Returns all missions in the dataset with a given status. |
| 51 | + * If there are no missions, return an empty collection. |
| 52 | + * |
| 53 | + * @param missionStatus the status of the missions |
| 54 | + * @throws IllegalArgumentException if missionStatus is null |
| 55 | + */ |
| 56 | + Collection<Mission> getAllMissions(MissionStatus missionStatus); |
| 57 | + |
| 58 | + /** |
| 59 | + * Returns the company with the most successful missions in a given time period. |
| 60 | + * Success is defined as MissionStatus.SUCCESS. |
| 61 | + * If multiple companies have the same number of successful missions, return any of them. |
| 62 | + * If there are no successful missions in the period, return an empty string. |
| 63 | + * If there are no missions at all, return an empty string. |
| 64 | + * |
| 65 | + * @param from the inclusive beginning of the time frame |
| 66 | + * @param to the inclusive end of the time frame |
| 67 | + * @throws IllegalArgumentException if from or to is null |
| 68 | + * @throws TimeFrameMismatchException if to is before from |
| 69 | + */ |
| 70 | + String getCompanyWithMostSuccessfulMissions(LocalDate from, LocalDate to); |
| 71 | + |
| 72 | + /** |
| 73 | + * Groups missions by country. |
| 74 | + * If there are no missions, return an empty map. |
| 75 | + */ |
| 76 | + Map<String, Collection<Mission>> getMissionsPerCountry(); |
| 77 | + |
| 78 | + /** |
| 79 | + * Returns the top N least expensive missions, ordered from cheapest to more expensive. |
| 80 | + * If there are no missions, return an empty list. |
| 81 | + * |
| 82 | + * @param n the number of missions to be returned |
| 83 | + * @param missionStatus the status of the missions |
| 84 | + * @param rocketStatus the status of the rockets |
| 85 | + * @throws IllegalArgumentException if n is less than or equal to 0, missionStatus or rocketStatus is null |
| 86 | + */ |
| 87 | + List<Mission> getTopNLeastExpensiveMissions(int n, MissionStatus missionStatus, RocketStatus rocketStatus); |
| 88 | + |
| 89 | + /** |
| 90 | + * Returns the most desired location for each company. |
| 91 | + * Most desired = location with the highest number of missions for that company. |
| 92 | + * Location is defined as the value in the "Location" column (e.g., "Kennedy Space Center, FL, USA"). |
| 93 | + * If a company has multiple locations with the same count, return any of them. |
| 94 | + * If there are no missions, return an empty map. |
| 95 | + * |
| 96 | + * @return a map where keys are company names and values are their most used mission locations |
| 97 | + */ |
| 98 | + Map<String, String> getMostDesiredLocationForMissionsPerCompany(); |
| 99 | + |
| 100 | + /** |
| 101 | + * Returns the location with most successful missions for each company in a given time period. |
| 102 | + * Successful = MissionStatus.SUCCESS. |
| 103 | + * For each company, finds the location where that company had the most successful missions. |
| 104 | + * If a company has multiple locations with the same count of successful missions, return any of them. |
| 105 | + * If a company has no successful missions in the period, it is NOT included in the result. |
| 106 | + * If there are no missions at all, return an empty map. |
| 107 | + * |
| 108 | + * @param from the inclusive beginning of the time frame (inclusive) |
| 109 | + * @param to the inclusive end of the time frame (inclusive) |
| 110 | + * @return a map where keys are company names and values are their locations with most successful missions in the period |
| 111 | + * @throws IllegalArgumentException if from or to is null |
| 112 | + * @throws TimeFrameMismatchException if to is before from |
| 113 | + */ |
| 114 | + Map<String, String> getLocationWithMostSuccessfulMissionsPerCompany(LocalDate from, LocalDate to); |
| 115 | + |
| 116 | + /** |
| 117 | + * Returns all rockets in the dataset. |
| 118 | + * If there are no rockets, return an empty collection. |
| 119 | + */ |
| 120 | + Collection<Rocket> getAllRockets(); |
| 121 | + |
| 122 | + /** |
| 123 | + * Returns the top N tallest rockets, in decreasing order. |
| 124 | + * If there are no rockets, return an empty list. |
| 125 | + * |
| 126 | + * @param n the number of rockets to be returned |
| 127 | + * @throws IllegalArgumentException if n is less than or equal to 0 |
| 128 | + */ |
| 129 | + List<Rocket> getTopNTallestRockets(int n); |
| 130 | + |
| 131 | + /** |
| 132 | + * Returns a mapping of rockets (by name) to their respective wiki page (if present). |
| 133 | + * If there are no rockets, return an empty map. |
| 134 | + */ |
| 135 | + Map<String, Optional<String>> getWikiPageForRocket(); |
| 136 | + |
| 137 | + /** |
| 138 | + * Returns the wiki pages for the rockets used in the N most expensive missions. |
| 139 | + * If there are no missions, return an empty list. |
| 140 | + * |
| 141 | + * @param n the number of missions to be returned |
| 142 | + * @param missionStatus the status of the missions |
| 143 | + * @param rocketStatus the status of the rockets |
| 144 | + * @throws IllegalArgumentException if n is less than or equal to 0, or missionStatus or rocketStatus is null |
| 145 | + */ |
| 146 | + List<String> getWikiPagesForRocketsUsedInMostExpensiveMissions(int n, MissionStatus missionStatus, |
| 147 | + RocketStatus rocketStatus); |
| 148 | + |
| 149 | + /** |
| 150 | + * Saves the name of the most reliable rocket in a given time period in an encrypted format. |
| 151 | + * |
| 152 | + * @param outputStream the output stream where the encrypted result is written into |
| 153 | + * @param from the inclusive beginning of the time frame |
| 154 | + * @param to the inclusive end of the time frame |
| 155 | + * @throws IllegalArgumentException if outputStream, from or to is null |
| 156 | + * @throws CipherException if the encrypt/decrypt operation cannot be completed successfully |
| 157 | + * @throws TimeFrameMismatchException if to is before from |
| 158 | + */ |
| 159 | + void saveMostReliableRocket(OutputStream outputStream, LocalDate from, LocalDate to) throws CipherException; |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +#### Record `Mission` |
| 164 | + |
| 165 | +Една мисия се моделира от следния `record`: |
| 166 | + |
| 167 | +```java |
| 168 | +Mission(String id, String company, String location, LocalDate date, Detail detail, RocketStatus rocketStatus, Optional<Double> cost, MissionStatus missionStatus) |
| 169 | +``` |
| 170 | + |
| 171 | +В нея, един **Detail** се моделира от следния `record`: |
| 172 | + |
| 173 | +#### Record `Detail` |
| 174 | + |
| 175 | +```java |
| 176 | +public record Detail(String rocketName, String payload) |
| 177 | +``` |
| 178 | + |
| 179 | +който се състои от двa компонента, разделени в data set-a един от друг с "|". Форматът е: `<rocketName>|<payload>`. |
| 180 | + |
| 181 | +Възможните резултати за всяка мисия са един от `Success, Failure, Partial Failure, Prelaunch Failure` и се моделират от следния `enum`: |
| 182 | + |
| 183 | +#### Enum `MissionStatus` |
| 184 | + |
| 185 | +```java |
| 186 | +package bg.sofia.uni.fmi.mjt.space.mission; |
| 187 | + |
| 188 | +public enum MissionStatus { |
| 189 | + SUCCESS("Success"), |
| 190 | + FAILURE("Failure"), |
| 191 | + PARTIAL_FAILURE("Partial Failure"), |
| 192 | + PRELAUNCH_FAILURE("Prelaunch Failure"); |
| 193 | + |
| 194 | + private final String value; |
| 195 | + |
| 196 | + MissionStatus(String value) { |
| 197 | + this.value = value; |
| 198 | + } |
| 199 | + |
| 200 | + public String toString() { |
| 201 | + return value; |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +#### Record `Rocket` |
| 207 | + |
| 208 | +Една ракета се моделира от следния record: |
| 209 | + |
| 210 | +```java |
| 211 | +public record Rocket(String id, String name, Optional<String> wiki, Optional<Double> height) |
| 212 | +``` |
| 213 | + |
| 214 | +След дадена масия, изполваната ракета може да бъде все още активна (**StatusActive**), или вече да не е в експлоатация (**StatusRetired**). Моделираме го със следния `enum`: |
| 215 | + |
| 216 | +#### Enum `RocketStatus` |
| 217 | + |
| 218 | +```java |
| 219 | +package bg.sofia.uni.fmi.mjt.space.rocket; |
| 220 | + |
| 221 | +public enum RocketStatus { |
| 222 | + STATUS_RETIRED("StatusRetired"), |
| 223 | + STATUS_ACTIVE("StatusActive"); |
| 224 | + |
| 225 | + private final String value; |
| 226 | + |
| 227 | + RocketStatus(String value) { |
| 228 | + this.value = value; |
| 229 | + } |
| 230 | + |
| 231 | + public String toString() { |
| 232 | + return value; |
| 233 | + } |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +Трите record-a: `Mission`, `Detail` и `Rocket` трябва да имат публичен каноничен конструктор. |
| 238 | + |
| 239 | +#### Rocket reliability |
| 240 | + |
| 241 | +Reliability-то на дадена ракета ще пресмятаме по следната формула: |
| 242 | + |
| 243 | +``` |
| 244 | +(2 * (броя на успешните мисии на ракетата) + (броя на неуспешните мисии на ракетата)) / (2 * (броя на всички мисии на ракетата)) |
| 245 | +``` |
| 246 | + |
| 247 | +Неуспешна мисия считаме за такава със статус `MissionStatus.FAILURE`, `MissionStatus.PARTIAL_FAILURE` или `MissionStatus.PRELAUNCH_FAILURE`. |
| 248 | +Ракетите, които не са участвали в мисии, имат reliability 0.0. |
| 249 | + |
| 250 | +> Пример: Ракета с 3 успешни мисии и 1 неуспешна: |
| 251 | +> Reliability = (2*3 + 1) / (2*4) = 7/8 = 0.875 |
| 252 | +
|
| 253 | +Алгоритъмът за криптиране (**AES**) има имплементация в JDK-то (в `javax.crypto` пакета) и за него сме ви дали [code snippet](https://github.com/fmi/java-course/blob/master/07-io-streams-and-files/snippets/src/bg/sofia/uni/fmi/mjt/io/CipherExample.java). Създайте клас **Rijndael**, който има следния конструктор: |
| 254 | + |
| 255 | +```java |
| 256 | +/** |
| 257 | + * Encrypts/decrypts data using AES (Rijndael) algorithm with the provided secret key. |
| 258 | + * |
| 259 | + * @param secretKey the encryption/decryption key |
| 260 | + * @throws IllegalArgumentException if secretKey is null |
| 261 | + */ |
| 262 | +public Rijndael(SecretKey secretKey) |
| 263 | +``` |
| 264 | + |
| 265 | +и имплементира интерфейса: |
| 266 | + |
| 267 | +```java |
| 268 | +package bg.sofia.uni.fmi.mjt.space.algorithm; |
| 269 | + |
| 270 | +import java.io.InputStream; |
| 271 | +import java.io.OutputStream; |
| 272 | + |
| 273 | +public interface SymmetricBlockCipher { |
| 274 | + /** |
| 275 | + * Encrypts the data from inputStream and puts it into outputStream |
| 276 | + * |
| 277 | + * @param inputStream the input stream where the data is read from |
| 278 | + * @param outputStream the output stream where the encrypted result is written into |
| 279 | + * @throws CipherException if the encrypt/decrypt operation cannot be completed successfully |
| 280 | + */ |
| 281 | + void encrypt(InputStream inputStream, OutputStream outputStream) throws CipherException; |
| 282 | + |
| 283 | + /** |
| 284 | + * Decrypts the data from inputStream and puts it into outputStream |
| 285 | + * |
| 286 | + * @param inputStream the input stream where the data is read from |
| 287 | + * @param outputStream the output stream where the decrypted result is written into |
| 288 | + * @throws CipherException if the encrypt/decrypt operation cannot be completed successfully |
| 289 | + */ |
| 290 | + void decrypt(InputStream inputStream, OutputStream outputStream) throws CipherException; |
| 291 | +} |
| 292 | +``` |
| 293 | + |
| 294 | +### Тестване |
| 295 | + |
| 296 | +Създайте автоматични тестове, с които да тествате решението си. |
| 297 | + |
| 298 | +### Структура на проекта |
| 299 | + |
| 300 | +Спазвайте имената на пакетите на всички по-долу описани класове, тъй като в противен случай решението ви няма да може да бъде тествано от грейдъра. |
| 301 | + |
| 302 | +```bash |
| 303 | +src |
| 304 | +└── bg.sofia.uni.fmi.mjt.space |
| 305 | + ├── algorithm |
| 306 | + │ ├── Rijndael.java |
| 307 | + │ └── SymmetricBlockCipher.java |
| 308 | + ├── exception |
| 309 | + │ ├── CipherException.java |
| 310 | + │ └── TimeFrameMismatchException.java |
| 311 | + ├── mission |
| 312 | + │ ├── Detail.java |
| 313 | + │ ├── Mission.java |
| 314 | + │ └── MissionStatus.java |
| 315 | + ├── rocket |
| 316 | + │ ├── Rocket.java |
| 317 | + │ └── RocketStatus.java |
| 318 | + ├── MJTSpaceScanner.java |
| 319 | + ├── SpaceScannerAPI.java |
| 320 | + └── (...) |
| 321 | +test |
| 322 | +└── bg.sofia.uni.fmi.mjt.space |
| 323 | + └── (...) |
| 324 | +``` |
| 325 | + |
| 326 | +Обърнете внимание, че при качване на решението ви, в грейдъра ще се изпълни само _smoke_ тест, чиято цел е да изчистите |
| 327 | +евентуални проблеми с компилацията. Референтите тестове и Checkstyle статичният код анализ ще се изпълнят еднократно |
| 328 | +след изтичане на крайния срок за предаване. За функционалната коректност и качеството на кода ще трябва да се погрижите |
| 329 | +без тяхната помощ. |
| 330 | + |
| 331 | +### :warning: Важно! |
| 332 | + |
| 333 | +⚡ 1. Уверете се, че решението ви се компилира **в грейдъра**. Има ясна индикация за това като го качвате. Ако не го докарате до успешна компилация преди крайния срок, решението ви няма да бъде преглеждано и ще бъде оценено с нула точки. |
| 334 | + |
| 335 | +⚡ 2. Уверете се, че решението ви зарежда и работи успешно с **целия оригинален data set**, наличен в `/resources`, а не само с някакъв внимателно подбран от вас subset. Ако при изпълнение на референтните тестове зареждането на data set-a фейлне с решението ви, ще фелйнат и всички тестове, и ще финиширате с нула точки за това домашно. |
| 336 | + |
| 337 | +### Предаване |
| 338 | + |
| 339 | +Качете `src` и `test` директориите на проекта (или `.zip` архив, който ги съдържа) в съответния assignment в грейдъра. Ако ползвате статични файлове за тестване, те трябва да се намират директно на нивото на `src` и `test`, без да са в отделна директория. Препоръчваме обаче да създадете автоматични тестове, които не разчитат на статични файлове. |
| 340 | + |
| 341 | +### Оценяване |
| 342 | + |
| 343 | +Решението може да ви донесе до 100 точки, като ще бъде оценявано за: |
| 344 | + |
| 345 | +* функционална пълнота и коректност, и за автоматични тестове с добър code coverage (60% от оценката) |
| 346 | +* добър обектно-ориентиран дизайн, спазване на правилата за чист код и подбиране на оптимални за задачата структури от данни (40% от оценката) |
| 347 | + |
| 348 | +### 🤖 Отговорно използване на AI и академична почтеност |
| 349 | + |
| 350 | +Използването на генеративни AI инструменти (като GitHub Copilot, ChatGPT и др.) е допустимо единствено с цел подпомагане на процеса на учене, но не и като заместител на самостоятелното мислене и работа. Всеки студент носи пълна отговорност за разбирането, тестването и обяснението на кода, който предава. Представянето на код, който е очевидно автоматично генериран или който не можете да обясните и защитите устно или писмено, ще се счита за форма на недопустимо подпомагане или плагиатство, съгласно правилата на курса и университета. Ако сте използвали AI, посочете това в документацията – уточнете кои части са генерирани, с каква цел, и опишете накратко как работят и как сте проверили тяхната коректност, по същия начин, по който бихте цитирали външен източник. Целта на тази политика е да насърчи отговорната и критична употреба на съвременни инструменти, задълбоченото разбиране на материала и поддържането на високи стандарти на академична почтеност. |
| 351 | + |
| 352 | +### Желаем ви успех! :four_leaf_clover: |
0 commit comments