Информациони системи 1/Лаб 2 2022 јануар

Извор: SI Wiki
Пређи на навигацију Пређи на претрагу

Прва надокнада друге лабораторијске вежбе 2022. године одржана је почетком фебруара 2022. године. База је такође била идентична као претходних година.

Поставка

За део базе података факултета креирати сервис са следећим крајњим тачкама:

  • (8 поена) POST .../prijava/{idPredmeta}
    • реqуест бодy: празно
    • респонсе бодy: празно
    • Право за извршавање ове методе има само студент. Студент може да пријави предмет само уколико прати тај предмет, за текући рок је омогућена пријава предмета и рок пријаве предмета је у истом семестру као и сам предмет.
  • (4 поена) POST .../admin/
    • реqуест бодy: (text/xml) Све информације су у следећем формату:
      <korisnik>
          <imeKor>...</imeKor>
          <sifra>...</sifra>
      </korisnik>
      
    • респонсе бодy: празно
    • Право за извршавање ове методе има само админ. Метода креира новог админа са датим информацијама. Није дозвољено да постоји више од једног админа са истим корисничким именом.

ЕР дијаграм

База и скрипта испод су идентични као на другој лабораторијској вежби 2020. године.

На слици је дат модел дела базе података факултета.

Дати ЕР дијаграм базе података.

Статус у semestar има вредности:

  • N — није у току
  • P — омогућена нова праћења предмета
  • T — у току

Статус у rok има вредности:

  • N — није у току
  • P — омогућена пријава предмета
  • T — у току

СQЛ

Следећа СQЛ скрипта прави базу приказану на дијаграму изнад заједно са подацима који могу да се користе за тестирање.

CREATE TABLE `korisnik` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `korisnicko_ime` VARCHAR(45),
    `sifra` VARCHAR(45)
);
CREATE TABLE `admin` (
    `korisnik_id` INT PRIMARY KEY,
    FOREIGN KEY (`korisnik_id`) REFERENCES `korisnik` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `student` (
    `korisnik_id` INT PRIMARY KEY,
    `indeks` VARCHAR(45),
    `ime_prezime` VARCHAR(45) NOT NULL,
    `godina` INT,
    FOREIGN KEY (`korisnik_id`) REFERENCES `korisnik` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `nastavnik` (
    `korisnik_id` INT PRIMARY KEY,
    `ime_prezime` VARCHAR(45),
    FOREIGN KEY (`korisnik_id`) REFERENCES `korisnik` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `semestar` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `naziv` VARCHAR(45) NOT NULL,
    `status` VARCHAR(1)
);
CREATE TABLE `predmet` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `sifra` VARCHAR(45) NOT NULL,
    `naziv` VARCHAR(45) NOT NULL,
    `semestar_id` INT NOT NULL,
    `godina` INT,
    FOREIGN KEY (`semestar_id`) REFERENCES `semestar` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `predaje` (
    `nastavnik_korisnik_id` INT,
    `predmet_id` INT,
    PRIMARY KEY (`nastavnik_korisnik_id`, `predmet_id`),
    FOREIGN KEY (`nastavnik_korisnik_id`) REFERENCES `nastavnik` (`korisnik_id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION,
    FOREIGN KEY (`predmet_id`) REFERENCES `predmet` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `prati` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `predmet_id` INT NOT NULL,
    `student_korisnik_id` INT NOT NULL,
    FOREIGN KEY (`predmet_id`) REFERENCES `predmet` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION,
    FOREIGN KEY (`student_korisnik_id`) REFERENCES `student` (`korisnik_id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `rok` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `naziv` VARCHAR(45) NOT NULL,
    `semestar_id` INT NOT NULL,
    `status` VARCHAR(1),
    FOREIGN KEY (`semestar_id`) REFERENCES `semestar` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `prijava` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `prati_id` INT NOT NULL,
    `rok_id` INT NOT NULL,
    FOREIGN KEY (`prati_id`) REFERENCES `prati` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION,
    FOREIGN KEY (`rok_id`) REFERENCES `rok` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);
CREATE TABLE `ocena` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `ocena` INT,
    `prijava_id` INT NOT NULL,
    FOREIGN KEY (`prijava_id`) REFERENCES `prijava` (`id`)
        ON DELETE CASCADE
        ON UPDATE NO ACTION
);

INSERT INTO `korisnik` (`korisnicko_ime`, `sifra`) VALUES
('admin', 'admin'),         -- 1
('pera', 'peric'),          -- 2
('mika', 'mikic'),          -- 3
('zika', 'zikic'),          -- 4
('cmilos', 'cmilos'),       -- 5
('tasha', 'tasha'),         -- 6
('stubic', 'stubic'),       -- 7
('tartalja', 'tartalja');   -- 8

INSERT INTO `admin` (`korisnik_id`) VALUES (1);

INSERT INTO `student` (`korisnik_id`, `indeks`, `ime_prezime`, `godina`) VALUES
(2, '0001', 'Pera Perić', 2019),
(3, '0002', 'Mika Mikić', 2020),
(4, '0010', 'Žika Žikić', 2018);

INSERT INTO `nastavnik` (`korisnik_id`, `ime_prezime`) VALUES
(5, 'Miloš Cvetanović'),
(6, 'Tamara Šekularac'),
(7, 'Stefan Tubić'),
(8, 'Igor Tartalja');

INSERT INTO `semestar` (`naziv`, `status`) VALUES
('Peti semestar 2019', 'N'),    -- 1
('Peti semestar 2021', 'T'),    -- 2
('Treći semestar 2021', 'T'),   -- 3
('Drugi semestar 2022', 'P'),   -- 4
('Četvrti semestar 2022', 'P'); -- 5

INSERT INTO `predmet` (`sifra`, `naziv`, `semestar_id`, `godina`) VALUES
('13S113IS1', 'Informacioni sistemi 1', 2, 2021),                   -- 1
('13E114IS1', 'Informacioni sistemi 1', 2, 2021),                   -- 2
('13S112OO1', 'Objektno orijentisano programiranje 1', 3, 2021),    -- 3
('13E112OO1', 'Objektno orijentisano programiranje 1', 3, 2021),    -- 4
('13S112OO2', 'Objektno orijentisano programiranje 2', 5, 2022),    -- 5
('13E112OO2', 'Objektno orijentisano programiranje 2', 5, 2022);    -- 6

INSERT INTO `predaje` (`nastavnik_korisnik_id`, `predmet_id`) VALUES
(8, 3),
(8, 4),
(8, 5),
(8, 6),
(5, 1),
(5, 2),
(6, 1),
(6, 2),
(7, 1),
(7, 2);

INSERT INTO `prati` (`predmet_id`, `student_korisnik_id`) VALUES
(1, 2), -- 1: Pera prati IS1
(2, 3), -- 2: Mika prati IS1
(3, 3), -- 3: Mika prati OO1
(3, 4); -- 4: Žika prati OO1

INSERT INTO `rok` (`naziv`, `semestar_id`, `status`) VALUES
('Januar', 1, 'N'),     -- 1
('Januar', 2, 'T'),     -- 2
('Januar', 3, 'T'),     -- 3
('Februar', 2, 'P'),    -- 4
('Februar', 3, 'P');    -- 5

INSERT INTO `prijava` (`prati_id`, `rok_id`) VALUES
(1, 2), -- 1: Pera prijavio IS1 za januar
(1, 4), -- 2: Pera prijavio IS1 za februar
(2, 2), -- 3: Mika prijavio IS1 za februar
(3, 5), -- 4: Mika prijavio OO1 za februar
(4, 3); -- 5: Žika prijavio OO1 za januar

INSERT INTO `ocena` (`ocena`, `prijava_id`) VALUES
(5, 1),
(10, 5);

Решење

persistence.xml

Дефинише јединицу перзистенције mypu. Претпоставља се постојање ресурса fakultetResource који је повезан на одговарајући Цоннецтион Поол на Глассфисх.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="mypu" transaction-type="JTA">
        <jta-data-source>fakultetResource</jta-data-source>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
    </persistence-unit>
</persistence>

models пакет

Овде су генерисане класе ентитета из базе приложене изнад са подразумеваним подешавањима.

filters пакет

BasicAuthFilter.java

Филтер који проверава да ли је корисник улогован и забрањује приступ уколико није, а кроз заглавље X-User-ID прослеђује ИД пријављеног корисника ресурсу. Могућа ситуација јесте да корисник зада своје X-User-ID заглавље како би опонашао неког наставника па се у ресурсу појаве два таква заглавља, и таквим случајем руководимо у ресурсима у којима је битно који је корисник пријављен. Филтер такође проверава којем се ресурсу приступа и одбија захтев уколико се приступа ресурсу коме тренутна улога не дозвољава приступ.

package filters;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import models.Korisnik;

@Provider
public class BasicAuthFilter implements ContainerRequestFilter {
    @PersistenceContext(unitName = "mypu")
    EntityManager em;

    @Override
    public void filter(ContainerRequestContext context) throws IOException {
        context.getHeaders().getFirst("Authorization");
        MultivaluedMap<String, String> headers = context.getHeaders();
        if (!headers.containsKey("Authorization")) {
            context.abortWith(
                Response
                    .status(Response.Status.UNAUTHORIZED)
                    .entity("Korisničko ime i lozinka nisu prosleđeni.")
                    .build()
            );
            return;
        }
        List<String> authHeaders = context.getHeaders().get("Authorization");
        if (authHeaders.isEmpty()) {
            context.abortWith(
                Response
                    .status(Response.Status.UNAUTHORIZED)
                    .entity("Korisničko ime i lozinka nisu prosleđeni.")
                    .build()
            );
            return;
        }
        String[] authorization = new String(Base64.getDecoder().decode(authHeaders.get(0).replace("Basic ", "")), StandardCharsets.UTF_8).split(":");
        if (authorization.length != 2) {
            context.abortWith(
                Response
                    .status(Response.Status.BAD_REQUEST)
                    .entity("Pogrešno prosleđeno korisničko ime ili lozinka.")
                    .build()
            );
            return;            
        }
        String username = authorization[0];
        String password = authorization[1];
        List<Korisnik> korisnici = em.createNamedQuery("Korisnik.findByKorisnickoIme", Korisnik.class)
            .setParameter("korisnickoIme", username)
            .getResultList();
        if (korisnici.isEmpty() || !korisnici.get(0).getSifra().equals(password)) {
            context.abortWith(
                Response
                    .status(Response.Status.BAD_REQUEST)
                    .entity("Pogrešno korisničko ime ili lozinka.")
                    .build()
            );
            return;
        }
        // Pass headers to resources.
        Korisnik korisnik = korisnici.get(0);
        String endpoint = context
            .getUriInfo()
            .getPathSegments()
            .get(0)
            .getPath();
        if (
            (endpoint.equals("prijava") && korisnik.getStudent() == null) ||
            (endpoint.equals("admin") && korisnik.getAdmin() == null)
        ) {
            context.abortWith(
                Response
                    .status(Response.Status.FORBIDDEN)
                    .entity("Nije vam dozvoljen pristup ovom resursu.")
                    .build()
            );
            return;
        }
        context.getHeaders().add("X-User-ID", korisnik.getId().toString());
    }
}

resources пакет

PrijavaResource.java

Руководи захтевом из првог задатка.

package resources;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import models.Prati;
import models.Predmet;
import models.Prijava;
import models.Rok;

@Stateless
@Path("prijava")
public class PrijavaResource {
    @PersistenceContext(unitName = "mypu")
    EntityManager em;
    
    @POST
    @Path("{idPredmeta}")
    public Response prijavi(@PathParam("idPredmeta") int idPredmeta, @Context HttpHeaders headers) {
        List<String> idHeaders = headers.getRequestHeaders().get("X-User-ID");
        if (idHeaders.size() != 1) {
            // The user may have passed our internal header in the request as
            // an attempt to identify as someone else.
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Zaglavlje X-User-ID je interno i ne sme se slati u zahtevima.")
                .build();
        }
        List<Rok> rokovi = em
            .createNamedQuery("Rok.findByStatus")
            .setParameter("status", "P")
            .getResultList();
        if (rokovi.isEmpty()) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Prijava nije u toku.")
                .build();
        }
        Rok rok = rokovi.get(0);
        int idStudenta = Integer.parseInt(idHeaders.get(0));
        Predmet predmet = em.find(Predmet.class, idPredmeta);
        if (predmet == null) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Ne postoji zadati predmet.")
                .build();
        }
        Prati prati = null;
        for (Prati p : predmet.getPratiList()) {
            if (p.getStudentKorisnikId().getKorisnikId() == idStudenta) {
                prati = p;
                break;
            }
        }
        if (prati == null) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Student ne prati predmet.")
                .build();
        }
        if (!predmet.getSemestarId().equals(rok.getSemestarId())) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Predmet i rok nisu u istom semestru.")
                .build();
        }
        for (Prijava prijava : prati.getPrijavaList()) {
            if (prijava.getRokId().equals(rok)) {
                return Response
                    .status(Response.Status.BAD_REQUEST)
                    .entity("Student je već prijavljen.")
                    .build();
            }
        }
        Prijava prijava = new Prijava();
        prijava.setPratiId(prati);
        prijava.setRokId(rok);
        em.persist(prijava);
        return Response.noContent().build();
    }
}

AdminResource.java

Руководи захтевом из другог задатка.

package resources;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import models.Admin;
import models.Korisnik;
import requests.CreateAdminRequest;

@Stateless
@Path("admin")
public class AdminResource {
    @PersistenceContext(unitName = "mypu")
    EntityManager em;

    @POST
    public Response napraviAdmina(CreateAdminRequest request) {
        if (request == null) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Podaci o novom adminu nisu ispravno prosleđeni.")
                .build();
        }
        List<Korisnik> korisnici = em
            .createNamedQuery("Korisnik.findByKorisnickoIme")
            .setParameter("korisnickoIme", request.imeKor)
            .getResultList();
        if (korisnici.stream().anyMatch(k -> k.getAdmin() != null)) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Admin sa zadatim korisničkim imenom već postoji.")
                .build();
        }
        Korisnik noviKorisnik = new Korisnik();
        noviKorisnik.setKorisnickoIme(request.imeKor);
        noviKorisnik.setSifra(request.sifra);
        em.persist(noviKorisnik);
        // We're flushing writes to the database so JPA updates our User object
        // and we can successfully obtain the new ID.
        em.flush();
        Admin noviAdmin = new Admin();
        noviAdmin.setKorisnikId(noviKorisnik.getId());
        em.persist(noviAdmin);
        return Response
            .status(Response.Status.NO_CONTENT)
            .build();
    }
}

requests пакет

CreateAdminRequest.java

Служи за десеријализацију тела захтева за креирање админа, како би из овог објекта могли да се ваде подаци тог захтева.

package requests;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "korisnik")
public class CreateAdminRequest {
    public String imeKor;
    public String sifra;
}