PollController.java

package com.github.curriculeon.controller;

import com.github.curriculeon.model.Poll;
import com.github.curriculeon.model.PollOption;
import com.github.curriculeon.service.PollService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
import java.util.*;

@Controller
public class PollController {
    private final PollService pollService;

    @Autowired
    public PollController(PollService pollService) {
        this.pollService = pollService;
    }

    @GetMapping({"/", "/index"})
    public String index(Model model) {
        List<Poll> recent = pollService.recentPolls();
        model.addAttribute("polls", recent);
        model.addAttribute("now", Instant.now());
        return "index";
    }

    @GetMapping("/create")
    public String createForm() {
        return "create";
    }

    @PostMapping("/create")
    public String createSubmit(@RequestParam String question, @RequestParam(name = "option") List<String> options) {
        pollService.createPoll(question, options);
        return "redirect:/";
    }

    @GetMapping("/poll/{id}")
    public String viewPoll(@PathVariable Long id, Model model, HttpServletRequest request) {
        Poll poll = pollService.findById(id).orElseThrow(() -> new NoSuchElementException("Poll not found"));
        boolean visible = pollService.isResultsVisible(poll);
        model.addAttribute("poll", poll);
        model.addAttribute("resultsVisible", visible);
        if (visible) {
            Map<Long, Long> counts = pollService.countsForPoll(poll);
            long total = counts.values().stream().mapToLong(Long::longValue).sum();
            model.addAttribute("counts", counts);
            model.addAttribute("totalVotes", total);
        }

        // ensure a voter fingerprint cookie exists so frontend will send it on vote
        String fingerprint = null;
        if (request.getCookies() != null) {
            for (Cookie c : request.getCookies()) {
                if ("ANV_VOTER".equals(c.getName())) {
                    fingerprint = c.getValue();
                    break;
                }
            }
        }
        model.addAttribute("fingerprint", fingerprint);

        // If results are not globally visible, allow a voter who has just voted to see current counts
        boolean viewerHasVoted = false;
        if (!visible && fingerprint != null && !fingerprint.trim().isEmpty()) {
            viewerHasVoted = pollService.hasVoted(id, fingerprint);
            if (viewerHasVoted) {
                Map<Long, Long> counts = pollService.countsForPoll(poll);
                long total = counts.values().stream().mapToLong(Long::longValue).sum();
                model.addAttribute("counts", counts);
                model.addAttribute("totalVotes", total);
            }
        }
        model.addAttribute("viewerHasVoted", viewerHasVoted);
        return "poll";
    }

    @PostMapping("/poll/{id}/vote")
    public Object vote(@PathVariable Long id, @RequestParam Long optionId,
                       @CookieValue(value = "ANV_VOTER", required = false) String fingerprint,
                       HttpServletResponse response,
                       RedirectAttributes redirectAttributes,
                       HttpServletRequest request) {
        // if no fingerprint cookie, create one and set it for future requests
        if (fingerprint == null || fingerprint.trim().isEmpty()) {
            fingerprint = UUID.randomUUID().toString();
            Cookie cookie = new Cookie("ANV_VOTER", fingerprint);
            cookie.setPath("/");
            cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year
            response.addCookie(cookie);
        }
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        try {
            pollService.vote(id, optionId, fingerprint);
        } catch (IllegalStateException ex) {
            // duplicate vote detected
            if (isAjax) {
                Map<String, String> body = new HashMap<>();
                body.put("error", "You have already voted on this poll from this browser.");
                return ResponseEntity.status(HttpStatus.CONFLICT).body(body);
            } else {
                redirectAttributes.addFlashAttribute("error", "You have already voted on this poll from this browser.");
                return "redirect:/poll/" + id;
            }
        }

        if (isAjax) {
            Map<String, String> body = new HashMap<>();
            body.put("message", "Thanks — your anonymous vote was recorded.");
            return ResponseEntity.ok(body);
        } else {
            redirectAttributes.addFlashAttribute("message", "Thanks — your anonymous vote was recorded.");
            return "redirect:/poll/" + id;
        }
    }

    @GetMapping("/poll/{id}/counts")
    @ResponseBody
    public Map<String, Object> counts(@PathVariable Long id) {
        Poll poll = pollService.findById(id).orElseThrow(() -> new NoSuchElementException("Poll not found"));
        Map<Long, Long> counts = pollService.countsForPoll(poll);
        long total = counts.values().stream().mapToLong(Long::longValue).sum();
        Map<String, Object> resp = new HashMap<>();
        resp.put("counts", counts);
        resp.put("totalVotes", total);
        return resp;
    }

    @GetMapping("/health")
    @ResponseBody
    public ResponseEntity<String> health() {
        // Simple public health endpoint for external monitors. Keep lightweight.
        return ResponseEntity.ok("OK");
    }

}