Informacioni sistemi 1/Lab 2 2022

Izvor: SI Wiki
Pređi na navigaciju Pređi na pretragu

Druga laboratorijska vežba 2022. godine održana je u januaru 2022. godine i grupe su bile skoro identične, sa malim razlikama u prvoj stavci. Baza je takođe bila identična kao prethodnih godina.

Postavka

Za deo baze podataka fakulteta kreirati servis sa sledećim krajnjim tačkama:

  • (8 poena) POST .../prijava/{idPredmeta}
    • request body: prazno
    • response body: prazno
    • Pravo za izvršavanje ove metode ima samo student. Student može da prijavi predmet samo ukoliko prati taj predmet, rok je otvoren za prijavu predmeta i rok prijave predmeta je u istom semestru kao i sam predmet.
  • (4 poena) GET .../prijave/{idPredmeta}?idRoka
    • request body: prazno
    • response body: (text/xml) Sve prijave ispita u sledećem formatu:
      <prijave>
          <prijava>
              <imePrezimeStudenta>Ana Anić</imePrezimeStudenta>
              <brojIndeksaStudenta>2010/0001</brojIndeksaStudenta>
              <sifraPredmeta>P1</sifraPredmeta>
              <nazivPredmeta>Programiranje 1</nazivPredmeta>
              <nazivRoka>januar 2010</nazivRoka>
          </prijava>
          ...
      </prijave>
      
    • Pravo za izvršavanje ove metode ima nastavnik. Metoda vraća sve prijave ispita u jednom roku u formatu iznad. Ukoliko je zadat i parametar idRoka, prikazuje samo prijave za zadati rok.

ER dijagram

Baza i skripta ispod su identični kao na drugoj laboratorijskoj vežbi 2020. godine.

Na slici je dat model dela baze podataka fakulteta.

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

Status u semestar ima vrednosti:

  • N — nije u toku
  • P — omogućena nova praćenja predmeta
  • T — u toku

Status u rok ima vrednosti:

  • N — nije u toku
  • P — omogućena prijava predmeta
  • T — u toku

SQL

Sledeća SQL skripta pravi bazu prikazanu na dijagramu iznad zajedno sa podacima koji mogu da se koriste za testiranje.

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);

Rešenje

persistence.xml

Definiše jedinicu perzistencije mypu. Pretpostavlja se postojanje resursa fakultetResource koji je povezan na odgovarajući Connection Pool na Glassfish.

<?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 paket

Ovde su generisane klase entiteta iz baze priložene iznad sa podrazumevanim podešavanjima.

filters paket

BasicAuthFilter.java

Filter koji proverava da li je korisnik ulogovan i zabranjuje pristup ukoliko nije, a kroz zaglavlja X-User-ID i X-User-Role prosleđuje ID i ulogu prijavljenog korisnika resursu, respektivno. Moguća situacija jeste da korisnik zada svoje X-User-ID ili X-User-Role zaglavlje kako bi oponašao nekog nastavnika pa se u resursu pojave dva takva zaglavlja, i takvim slučajem rukovodimo u resursima u kojima je bitno koji je korisnik prijavljen, odnosno koja je njegova uloga.

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 paket

PrijavaResource.java

Rukovodi zahtevom iz prvog zadatka.

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

Rukovodi zahtevom iz drugog zadatka.

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 paket

PrijavaResponse.java

Koristi se kao element liste u PrijaveResponse klasi, odnosno sadrži sve potrebne informacije o jednoj prijavi koje treba da se vrate iz druge stavke zadatka.

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

Koristi se za formatiranje odgovora iz druge stavke zadatka kao XML, tako da individualne prijave budu unutar <prijava> a oko njih da bude <prijave>.

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;
    }
}