package elw.dao; import elw.dao.ctx.CtxSlot; import elw.dao.ctx.CtxTask; import elw.vo.Class; import elw.vo.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; public class Ctx implements elw.vo.Ctx { private static final Logger log = LoggerFactory.getLogger(Ctx.class); private static final char ELEM_ENR = 'e'; private static final char ELEM_INDEX_ENTRY = 'i'; private static final char ELEM_GROUP = 'g'; private static final char ELEM_STUD = 's'; private static final char ELEM_COURSE = 'c'; private static final char ELEM_ASS_TYPE = 't'; private static final char ELEM_ASS = 'a'; private static final char ELEM_VER = 'v'; private static final String order = "egscitav"; private static final String SEP = "--"; private final KeyVal<String, Enrollment> enr; private final KeyVal<String, Student> student; private final KeyVal<String, TaskType> assType; private final KeyVal<String, Task> ass; private final KeyVal<String, Version> ver; private final KeyVal<String, Course> course; private final KeyVal<String, Group> group; private final KeyVal<String, IndexEntry> indexEntry; private final Map<Character, KeyVal> elemToKeyVal = new TreeMap<Character, KeyVal>(); public Ctx( String enrId, String groupId, String studId, String courseId, String index, String assTypeId, String assId, String verId ) { enr = new KeyVal<String, Enrollment>(Enrollment.class, enrId); student = new KeyVal<String, Student>(Student.class, studId); assType = new KeyVal<String, TaskType>(TaskType.class, assTypeId); ass = new KeyVal<String, Task>(Task.class, assId); ver = new KeyVal<String, Version>(Version.class, verId); course = new KeyVal<String, Course>(Course.class, courseId); group = new KeyVal<String, Group>(Group.class, groupId); indexEntry = new KeyVal<String, IndexEntry>(IndexEntry.class, index); elemToKeyVal.put(ELEM_ENR, enr); elemToKeyVal.put(ELEM_STUD, student); elemToKeyVal.put(ELEM_ASS_TYPE, assType); elemToKeyVal.put(ELEM_ASS, ass); elemToKeyVal.put(ELEM_VER, ver); elemToKeyVal.put(ELEM_COURSE, course); elemToKeyVal.put(ELEM_GROUP, group); elemToKeyVal.put(ELEM_INDEX_ENTRY, indexEntry); } protected Ctx() { this(null, null, null, null, null, null, null, null); } public Task getAss() { return ass.getValue(); } public TaskType getAssType() { return assType.getValue(); } public Course getCourse() { return course.getValue(); } public Enrollment getEnr() { return enr.getValue(); } public Group getGroup() { return group.getValue(); } public Student getStudent() { return student.getValue(); } public Version getVer() { return ver.getValue(); } public String getAssTypeId() { return assType.getKey(); } public IndexEntry getIndexEntry() { return indexEntry.getValue(); } public Ctx removeAll(String elements) { final Ctx res = copy(); for (char element : elements.toCharArray()) { final KeyVal keyVal = res.elemToKeyVal.get(element); if (keyVal != null) { keyVal.clear(); } else { throw new IllegalArgumentException("no keyVal for element '" + element + "'"); } } return res; } public Ctx retainAll(String elements) { final Ctx res = copy(); for (char element : res.elemToKeyVal.keySet()) { final KeyVal keyVal = res.elemToKeyVal.get(element); if (elements.indexOf(element) >= 0) { continue; } keyVal.clear(); } return res; } public static Ctx fromString(final String path) { if (path == null || path.trim().length() == 0) { return new Ctx(); } final String[] comp = path.split(SEP); if (comp.length < 1) { return new Ctx(); } if (comp.length == 1) { throw new IllegalArgumentException("invalid context: '" + path + "'"); } final String format = comp[0]; if (format.length() + 1 != comp.length) { throw new IllegalArgumentException("format does not match content: '" + path + "'"); } Ctx ctx = new Ctx(); for (char elem : order.toCharArray()) { if (format.indexOf(elem) >= 0) { final String elemKeyStr = comp[format.indexOf(elem) + 1]; //noinspection unchecked ctx.elemToKeyVal.get(elem).setKey(elemKeyStr); } } return ctx; } public static Ctx forCourse(final Course course) { if (course == null) { throw new IllegalArgumentException("course is null"); } return new Ctx().extendCourse(course); } public static Ctx forEnr(final Enrollment enr) { if (enr == null) { throw new IllegalArgumentException("enr is null"); } return new Ctx().extEnr(enr); } public static Ctx forAssType(final Course course, final TaskType assType) { if (assType == null) { throw new IllegalArgumentException("task type is null"); } return forCourse(course).extendAssType(assType); } public static Ctx forAss(Course course, TaskType assType, Task ass) { if (ass == null) { throw new IllegalArgumentException("ass is null"); } return forAssType(course, assType).extendTask(ass); } public String ei() { return norm("ecgi") + SEP + getEnr().getId() + SEP + getIndexEntry().getId(); } public String es() { return norm("ecgs") + SEP + getEnr().getId() + SEP + getStudent().getId(); } public Ctx resolve(Queries queries) { if (enr.isPending()) { extEnr(queries.enrollmentSome(enr.getKey())); } if (course.isPending()) { extCourse(queries.course(course.getKey())); } if (group.isPending()) { extGroup(queries.group(group.getKey())); } return resolve(); } protected Ctx resolve() { // this does not involve DAO lookups at all if (group.isResolved() && student.isPending()) { student.setValue(group.getValue().getStudents().get(student.getKey())); } if (enr.isResolved() && indexEntry.isPending()) { final String indexKey = indexEntry.getKey(); final IndexEntry ieVal = enr.getValue().getIndex().get(indexKey); if (indexEntry.resolve(ieVal)) { assType.setKey(indexEntry.getValue().getTaskTypeId()); ass.setKey(indexEntry.getValue().getTaskId()); } } if (course.isResolved() && assType.isPending()) { assType.resolve(course.getValue().getTaskTypes().get(assType.getKey())); } if (assType.isResolved() && ass.isPending()) { ass.resolve(assType.getValue().getTasks().get(ass.getKey())); } if (student.isResolved() && ass.isResolved()) { final String verId = Nav.resolveVersion( ass.getValue(), indexEntry.getValue(), group.getValue(), student.getValue().getId() ).getId(); ver.setKey(verId); } if (ass.isResolved() && ver.isPending()) { ver.setValue(ass.getValue().getVersions().get(ver.getKey())); } return this; } private Ctx extEnr(final Enrollment newEnr) { if (enr.resolve(newEnr)) { course.setKey(enr.getValue().getCourseId()); group.setKey(enr.getValue().getGroupId()); } return this; } public Ctx extGroup(final Group newGroup) { if (isNull(newGroup)) { log.error("HOUSTON on newGroup.getId()"); } else if (isNull(newGroup.getId())) { log.error("HOUSTON on newGroup.getId().equals()"); } if (enr.isResolved() && !newGroup.getId().equals(enr.getValue().getGroupId())) { throw new IllegalArgumentException("enrollment/group mismatch"); } group.resolve(newGroup); return this; } public Ctx extCourse(final Course newCourse) { if (isNull(newCourse)) { log.error("HOUSTON on newCourse.getId()"); } else if (isNull(newCourse.getId())) { log.error("HOUSTON on newCourse.getId().equals()"); } if (enr.isResolved() && !newCourse.getId().equals(enr.getValue().getCourseId())) { throw new IllegalArgumentException("enrollment/course mismatch"); } course.resolve(newCourse); return this; } public Ctx extStudent(final Student newStud) { if (!group.isResolved()) { throw new IllegalStateException("group not resolved"); } if (group.getValue().getStudents().get(newStud.getId()) == null) { log.warn("group {} / student {} mismatch", group.getValue(), newStud); throw new IllegalArgumentException("group/student mismatch"); } student.resolve(newStud); return resolve(); } public Ctx extIndex(final String index) { if (!enr.isResolved()) { throw new IllegalStateException("enrollment not resolved"); } indexEntry.setKey(index); return resolve(); } public Ctx extIndexEntry(IndexEntry indexEntry) { if (!enr.isResolved()) { throw new IllegalStateException("enrollment not resolved"); } this.indexEntry.setKey(indexEntry.getId()); return resolve(); } private Ctx extAssType(final TaskType newType) { if (!course.isResolved()) { throw new IllegalStateException("course not set"); } if (course.getValue().getTaskTypes().get(newType.getId()) == null) { throw new IllegalArgumentException("course/aType mismatch"); } assType.resolve(newType); return resolve(); } private Ctx extTask(final Task newTask) { if (!assType.isResolved()) { throw new IllegalStateException("taskType not set"); } if (assType.getValue().getTasks().get(newTask.getId()) == null) { throw new IllegalArgumentException("taskType/task mismatch"); } ass.resolve(newTask); return resolve(); } public Ctx extVer(final Version newVer) { if (!ass.isResolved()) { throw new IllegalArgumentException("task not set"); } if (ass.getValue().getVersions().get(newVer.getId()) == null) { throw new IllegalArgumentException("task/ver mismatch"); } ver.resolve(newVer); return this; } public Ctx extendGroup(final Group newGroup) { return copy().extGroup(newGroup); } public Ctx extendCourse(final Course newCourse) { return copy().extCourse(newCourse); } public Ctx extendStudent(final Student newStud) { return copy().extStudent(newStud); } public Ctx extendIndex(final String index) { return copy().extIndex(index); } private Ctx extendAssType(final TaskType newType) { return copy().extAssType(newType); } public Ctx extendTask(final Task newTask) { return copy().extTask(newTask); } public Ctx extendVer(final Version newVer) { return copy().extVer(newVer); } private String getResolveState() { final StringBuilder res = new StringBuilder(); for (char elem : order.toCharArray()) { if (elemToKeyVal.get(elem).isResolved()) { res.append((elem)); } } return res.toString(); } public String toString() { final String format = norm(getResolveState()); if (format.isEmpty()) { return ""; } final StringBuilder res = new StringBuilder(format); for (char comp : format.toCharArray()) { res.append(SEP); res.append(elemToKeyVal.get(comp).getKey()); } return res.toString(); } @SuppressWarnings({"RedundantIfStatement"}) @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ctx ctx = (Ctx) o; if (!ass.equals(ctx.ass)) return false; if (!assType.equals(ctx.assType)) return false; if (!course.equals(ctx.course)) return false; if (!enr.equals(ctx.enr)) return false; if (!group.equals(ctx.group)) return false; if (!indexEntry.equals(ctx.indexEntry)) return false; if (!student.equals(ctx.student)) return false; if (!ver.equals(ctx.ver)) return false; return true; } @Override public int hashCode() { int result = enr.hashCode(); result = 31 * result + student.hashCode(); result = 31 * result + assType.hashCode(); result = 31 * result + ass.hashCode(); result = 31 * result + ver.hashCode(); result = 31 * result + course.hashCode(); result = 31 * result + group.hashCode(); result = 31 * result + indexEntry.hashCode(); return result; } private String dump() { return "e:" + enr + " g:" + group + " s:" + student + " c:" + course + "; " + " i:" + indexEntry + " t:" + assType + " a:" + ass + " v:" + ver; } private Ctx copy() { final Ctx copy = new Ctx(); copy.enr.copyOf(enr); copy.group.copyOf(group); copy.student.copyOf(student); copy.course.copyOf(course); copy.indexEntry.copyOf(indexEntry); copy.assType.copyOf(assType); copy.ass.copyOf(ass); copy.ver.copyOf(ver); return copy; } public boolean resolved(final String state) { for (char elem : state.toCharArray()) { if (!elemToKeyVal.get(elem).isResolved()) { return false; } } return true; } @SuppressWarnings({"SimplifiableIfStatement"}) private static boolean isRedundant(String elemsBefore, char elemAfter) { if (elemsBefore.indexOf(ELEM_ENR) >= 0 && elemsBefore.indexOf(ELEM_STUD) >= 0 && elemsBefore.indexOf(ELEM_INDEX_ENTRY) >= 0 && elemAfter == ELEM_VER) { return true; } if (elemsBefore.indexOf(ELEM_ENR) >= 0 && (elemAfter == ELEM_COURSE || elemAfter == ELEM_GROUP)) { return true; } return elemsBefore.indexOf(ELEM_INDEX_ENTRY) >= 0 && (elemAfter == ELEM_ASS_TYPE || elemAfter == ELEM_ASS); } private static String removeRedundant(final String format) { boolean anyRedundant = false; boolean[] redundant = new boolean[format.length()]; for (int afterPos = 1; afterPos < format.length(); afterPos++) { char after = format.charAt(afterPos); if (isRedundant(format.substring(0, afterPos), after)) { anyRedundant = redundant[afterPos] = true; } } if (!anyRedundant) { return format; } final StringBuilder result = new StringBuilder(); result.append(format); for (int pos = redundant.length - 1; pos >= 0; pos--) { if (redundant[pos]) { result.deleteCharAt(pos); } } return result.toString(); } private static String reorder(final String format) { char[] result = null; for (int count = 0; count < format.length(); count++) { boolean reordered = false; for (int pos = 0; pos < format.length() - 1; pos++) { char cur; char next; if (result == null) { cur = format.charAt(pos); next = format.charAt(pos + 1); } else { cur = result[pos]; next = result[pos + 1]; } if (order.indexOf(cur) > order.indexOf(next)) { reordered = true; if (result == null) { result = format.toCharArray(); } result[pos] = next; result[pos + 1] = cur; } } if (!reordered) { break; } } return result != null ? String.valueOf(result) : format; } private static String norm(final String state) { return removeRedundant(reorder(state)); } public Class cFrom() { if (!resolved("eci")) { throw new IllegalStateException(this.toString()); } return CtxTask.classForKey( getEnr().getClasses(), getIndexEntry().getClassFrom() ); } public Class cDue(final String slotId) { if (!resolved("eci")) { throw new IllegalStateException(this.toString()); } final Map<String, String> due = getIndexEntry().getClassDue(); final String classDue = due == null ? null : due.get(slotId); if (classDue == null) { return null; } return CtxTask.classForKey( getEnr().getClasses(), classDue ); } // LATER move this to base.G4mat private static String renderBytes(byte[] checkSum) { final StringBuilder result = new StringBuilder(); for (byte checkByte : checkSum) { result.append(Integer.toString((checkByte & 0xff) + 0x100, 16).substring(1)); } return result.toString(); } public static String digest(String text) { try { final MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(text.getBytes("UTF-8"), 0, text.length()); return renderBytes(md.digest()); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } public String cmpNameNorm(Format f, FileSlot slot, FileBase file) { final Version ver = getVer(); String result; try { final String normName = getEnr().getName() + "-" + getStudent().getName() + "--" + getAss().getName() + (ver == null ? "" : "-" + ver.getName()) + "--" + slot.getName() + "-" + f.format(file.getStamp(), "MMdd-HHmm"); final String oriName = file.getName(); final String oriExt; if (oriName != null && oriName.trim().length() > 0) { final int oriLastDot = oriName.lastIndexOf("."); oriExt = oriLastDot < 0 ? "" : oriName.substring(oriLastDot); } else { oriExt = ".txt"; } final String normNameNoWs = normName.replaceAll("[\\s\\\\/]+", "_") + oriExt; result = URLEncoder.encode( normNameNoWs, "UTF-8" ); } catch (UnsupportedEncodingException e1) { throw new IllegalStateException("UTF-8 is NOT supported?!"); } return result; } public boolean checkRead(FileSlot slot, SortedMap<String, List<Solution>> filesStud) { final FileSlot fileSlot = getAssType().getFileSlots().get(slot.getId()); for (String slotIdRA : fileSlot.getReadApprovals()) { if (!isApprovedAny(slotIdRA, filesStud)) { return false; } } return true; } public boolean checkWrite(FileSlot slot, SortedMap<String, List<Solution>> filesStud) { final FileSlot fileSlot = getAssType().getFileSlots().get(slot.getId()); if (!fileSlot.isWritable()) { return false; } final List<String> writeApprovals = fileSlot.getWriteApprovals(); for (String slotIdWA : writeApprovals) { if (!isApprovedAny(slotIdWA, filesStud)) { return false; } } return true; } public static boolean isApprovedAny( final String slotId, final Map<String, List<Solution>> filesStud ) { final List<Solution> files = filesStud.get(slotId); if (files == null || files.isEmpty()) { return false; } boolean approved = false; for (Solution s : files) { if (s.getScore() != null && s.getScore().state() == State.APPROVED) { approved = true; } } return approved; } public static boolean isDeclinedLast( String slotId, final Map<String, List<Solution>> filesStud ) { final List<Solution> files = filesStud.get(slotId); if (files == null || files.size() == 0) { return false; } final Solution s = files.get(files.size() - 1); return s.getScore() != null && !(s.getScore().state() == State.APPROVED); } public static boolean isPendingLast( String slotId, final Map<String, List<Solution>> filesStud ) { final List<Solution> files = filesStud.get(slotId); if (files == null || files.isEmpty()) { return false; } final Solution f = files.get(files.size() - 1); return f.getScore() == null; } public CtxSlot ctxSlot(String slotId) { return ctxSlot(getAssType().getFileSlots().get(slotId)); } public CtxSlot ctxSlot(final FileSlot slot) { return new CtxSlot( getEnr(), getGroup(), getStudent(), getCourse(), getIndexEntry(), getAss(), getAssType(), getVer(), slot ); } protected class KeyVal<KeyType, ValueType> { private final java.lang.Class<ValueType> valueClass; private KeyType key; private ValueType value; public KeyVal(final java.lang.Class<ValueType> valueClass) { this(valueClass, null, null); } public KeyVal(final java.lang.Class<ValueType> valueClass, KeyType key) { this(valueClass, key, null); } public KeyVal(java.lang.Class<ValueType> valueClass, KeyType key, ValueType value) { this.key = key; this.value = value; this.valueClass = valueClass; } public KeyType getKey() { return key; } public void setKey(KeyType key) { this.key = key; } public ValueType getValue() { return value; } public void setValue(ValueType value) { this.value = value; } @SuppressWarnings("unchecked") public boolean resolve(ValueType value) { if (value == null) { log.warn("resolved " + valueClass.getSimpleName() + " to null: " + Ctx.this.dump()); return false; } if (value instanceof IdNamed && !(value instanceof IndexEntry)) { final String valueKey = ((IdNamed) value).getId(); if (key != null) { if (!key.equals(valueKey)) { throw new IllegalStateException( "resolved " + valueClass.getSimpleName() + " to a different object: " + Ctx.this.dump() ); } } else { this.key = (KeyType) valueKey; } } else { if (key == null) { throw new IllegalStateException( "please set " + valueClass.getSimpleName() + " key first: " + Ctx.this.dump() ); } } this.value = value; return true; } public void copyOf(KeyVal<KeyType, ValueType> that) { this.key = that.key; this.value = that.value; // LATER possible aliasing here } public void clear() { this.key = null; this.value = null; } public boolean isInited() { return key != null; } public boolean isResolved() { return value != null; } public boolean isPending() { return isInited() && !isResolved(); } @Override public String toString() { if (!isInited()) { return "?"; } return "'" + String.valueOf(key) + "'" + (isResolved() ? "" : "?"); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KeyVal val = (KeyVal) o; if (key != null ? !key.equals(val.key) : val.key != null) return false; return valueClass.equals(val.valueClass); } @Override public int hashCode() { int result = valueClass.hashCode(); result = 31 * result + (key != null ? key.hashCode() : 0); return result; } } private static boolean isNull(Object value) { return value == null; } }