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

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

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

Поставка

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

  • (8 поена) POST .../prijava/{idPredmeta}
    • реqуест бодy: празно
    • респонсе бодy: празно
    • Право за извршавање ове методе има само студент. Студент може да пријави предмет само уколико прати тај предмет, рок је отворен за пријаву предмета и рок пријаве предмета је у истом семестру као и сам предмет.
  • (4 поена) GET .../prijave/{idPredmeta}?idRoka
    • реqуест бодy: празно
    • респонсе бодy: (text/xml) Све пријаве испита у следећем формату:
      <prijave>
          <prijava>
              <imePrezimeStudenta>Ana Anić</imePrezimeStudenta>
              <brojIndeksaStudenta>2010/0001</brojIndeksaStudenta>
              <sifraPredmeta>P1</sifraPredmeta>
              <nazivPredmeta>Programiranje 1</nazivPredmeta>
              <nazivRoka>januar 2010</nazivRoka>
          </prijava>
          ...
      </prijave>
      
    • Право за извршавање ове методе има наставник. Метода враћа све пријаве испита у једном року у формату изнад. Уколико је задат и параметар idRoka, приказује само пријаве за задати рок.

ЕР дијаграм

База и скрипта испод су идентични као на другој лабораторијској вежби 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-Role прослеђује ИД и улогу пријављеног корисника ресурсу, респективно. Могућа ситуација јесте да корисник зада своје X-User-ID или X-User-Role заглавље како би опонашао неког наставника па се у ресурсу појаве два таква заглавља, и таквим случајем руководимо у ресурсима у којима је битно који је корисник пријављен, односно која је његова улога.

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.
        String role = "none";
        Korisnik korisnik = korisnici.get(0);
        if (korisnik.getAdmin() != null) {
            role = "admin";
        } else if (korisnik.getNastavnik() != null) {
            role = "nastavnik";
        } else if (korisnik.getStudent() != null) {
            role = "student";
        }
        context.getHeaders().add("X-User-ID", korisnik.getId().toString());
        context.getHeaders().add("X-User-Role", role);
    }
}

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");
        List<String> roleHeaders = headers.getRequestHeaders().get("X-User-Role");
        if (idHeaders.size() != 1 || roleHeaders.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("Zaglavlja X-User-ID i X-User-Role su interna i ne smeju se slati u zahtevima.")
                .build();
        }
        if (!roleHeaders.get(0).equals("student")) {
            return Response
                .status(Response.Status.FORBIDDEN)
                .entity("Morate biti student.")
                .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();
    }
}

PrijaveResource.java

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

package resources;

import java.util.ArrayList;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import models.Predmet;
import responses.PrijavaResponse;
import responses.PrijaveResponse;

@Stateless
@Path("prijave")
public class PrijaveResource {
    @PersistenceContext(unitName = "mypu")
    EntityManager em;
    
    @GET
    @Path("{idPredmeta}")
    @Produces(MediaType.TEXT_XML)
    public Response getPrijave(@PathParam("idPredmeta") int idPredmeta, @QueryParam("idRoka") Integer idRoka, @Context HttpHeaders headers) {
        List<String> roleHeaders = headers.getRequestHeaders().get("X-User-Role");
        if (roleHeaders.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-Role je interno i ne sme se slati u zahtevima.")
                .build();
        }
        if (!roleHeaders.get(0).equals("nastavnik")) {
            return Response
                .status(Response.Status.FORBIDDEN)
                .entity("Morate biti nastavnik.")
                .build();
        }
        Predmet predmet = em.find(Predmet.class, idPredmeta);
        if (predmet == null) {
            return Response
                .status(Response.Status.BAD_REQUEST)
                .entity("Ne postoji zadati predmet.")
                .build();
        }
        List<PrijavaResponse> prijave = new ArrayList<>();
        predmet.getPratiList().forEach(prati -> {
            prati.getPrijavaList().forEach(prijava -> {
                if (idRoka != null && !prijava.getRokId().getId().equals(idRoka)) {
                    return;
                }
                prijave.add(
                    new PrijavaResponse()
                        .setImePrezimeStudenta(prati.getStudentKorisnikId().getImePrezime())
                        .setBrojIndeksaStudenta(prati.getStudentKorisnikId().getIndeks())
                        .setSifraPredmeta(predmet.getSifra())
                        .setNazivPredmeta(predmet.getNaziv())
                        .setNazivRoka(prijava.getRokId().getNaziv())
                );
            });
        });
        PrijaveResponse prijaveResponse = new PrijaveResponse();
        prijaveResponse.setPrijave(prijave);
        return Response
            .ok(prijaveResponse)
            .build();
    }
}

responses пакет

PrijavaResponse.java

Користи се као елемент листе у PrijaveResponse класи, односно садржи све потребне информације о једној пријави које треба да се врате из друге ставке задатка.

package responses;

public class PrijavaResponse {
    private String imePrezimeStudenta;
    private String brojIndeksaStudenta;
    private String sifraPredmeta;
    private String nazivPredmeta;
    private String nazivRoka;
    public String getImePrezimeStudenta() {
        return imePrezimeStudenta;
    }
    public PrijavaResponse setImePrezimeStudenta(String imePrezimeStudenta) {
        this.imePrezimeStudenta = imePrezimeStudenta;
        return this;
    }
    public String getBrojIndeksaStudenta() {
        return brojIndeksaStudenta;
    }
    public PrijavaResponse setBrojIndeksaStudenta(String brojIndeksaStudenta) {
        this.brojIndeksaStudenta = brojIndeksaStudenta;
        return this;
    }
    public String getSifraPredmeta() {
        return sifraPredmeta;
    }
    public PrijavaResponse setSifraPredmeta(String sifraPredmeta) {
        this.sifraPredmeta = sifraPredmeta;
        return this;
    }
    public String getNazivPredmeta() {
        return nazivPredmeta;
    }
    public PrijavaResponse setNazivPredmeta(String nazivPredmeta) {
        this.nazivPredmeta = nazivPredmeta;
        return this;
    }
    public String getNazivRoka() {
        return nazivRoka;
    }
    public PrijavaResponse setNazivRoka(String nazivRoka) {
        this.nazivRoka = nazivRoka;
        return this;
    }
}

PrijaveResponse.java

Користи се за форматирање одговора из друге ставке задатка као XМЛ, тако да индивидуалне пријаве буду унутар <пријава> а око њих да буде <пријаве>.

package responses;

import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "prijave")
public class PrijaveResponse {
    private List<PrijavaResponse> prijave;
    @XmlElement(name = "prijava")
    public List<PrijavaResponse> getPrijave() {
        return prijave;
    }
    public void setPrijave(List<PrijavaResponse> prijave) {
        this.prijave = prijave;
    }
}