/* * Copyright (C) Jakub Neubauer, 2007 * * This file is part of TaskBlocks * * TaskBlocks is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * TaskBlocks is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package taskblocks.io; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import taskblocks.modelimpl.ColorLabel; import taskblocks.modelimpl.ManImpl; import taskblocks.modelimpl.TaskImpl; import taskblocks.modelimpl.TaskModelImpl; import taskblocks.utils.Pair; import taskblocks.utils.Utils; /** * Used to load/save the task project model * * @author jakub * */ public class ProjectSaveLoad { public static final String TASKMAN_E = "taskman"; public static final String TASKS_E = "tasks"; public static final String MANS_E = "mans"; public static final String TASK_E = "task"; public static final String MAN_E = "man"; public static final String PREDECESSORS_E = "predecessors"; public static final String PREDECESSOR_E = "predecessor"; public static final String VERSION_A = "version"; public static final String NAME_A = "name"; public static final String WORKLOAD_A = "workload"; public static final String ID_A = "id"; public static final String START_A = "start"; public static final String END_A = "end"; public static final String DURATION_A = "duration"; public static final String ACTUAL_A = "actualDuration"; public static final String MAN_A = "man"; public static final String PRED_A = "pred"; public static final String COLOR_A = "color"; public static final String COMM_A = "comment"; // Id in Bugzilla, used when exporting to it. public static final String BUGID_A = "bugid"; TaskModelImpl _model; Map<String, TaskImpl> _taskIds; Map<String, ManImpl> _manIds; public static final int CURRENT_VERSION = 1; /** * Loads project data model from given file. * TODO: checks for missing data in elements * * @param f * @return * @throws WrongDataException */ public TaskModelImpl loadProject(URL f) throws WrongDataException { InputStream input = null; try { input = f.openStream(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc; doc = dbf.newDocumentBuilder().parse(input); Element rootE = doc.getDocumentElement(); if(!TASKMAN_E.equals(rootE.getNodeName())) { throw new WrongDataException("Document is not TaskBlocks project"); } // mapping ID -> ManImpl Map<String, ManImpl> mans = new HashMap<String, ManImpl>(); // mapping ID -> TaskImpl Map<String, TaskImpl>tasks = new HashMap<String, TaskImpl>(); // mapping tasks -> list of their predecessors IDs List<Pair<TaskImpl, String[]>> taskPredecessorsIds = new ArrayList<Pair<TaskImpl,String[]>>(); // check the data version. The first Taskblocks files had no version attribute. String versionAttr = rootE.getAttribute(VERSION_A); if(versionAttr != null && versionAttr.trim().length() > 0) { try { versionAttr = versionAttr.trim(); int version = Integer.parseInt(versionAttr); if(version > CURRENT_VERSION) { throw new WrongDataException("Cannot load higher version '" + version + "', current is '" + CURRENT_VERSION + "'."); } } catch(NumberFormatException e) { throw new WrongDataException("Wrong data file version: '" + versionAttr + "'"); } } // 1. load all tasks and mans alone, 2. bind them between each other Element mansE = getFirstChild(rootE, MANS_E); if(mansE != null) { Element[] manEs = Utils.getChilds(mansE, MAN_E); for(Element manE: manEs) { String manName = manE.getAttribute(NAME_A); String manWorkload = manE.getAttribute(WORKLOAD_A); String manId = manE.getAttribute(ID_A); ManImpl man = new ManImpl(); man.setName(manName); if(manWorkload != null && manWorkload.trim().length() > 0) { try { man.setWorkload(Double.parseDouble(manWorkload.trim())/100.0); } catch(NumberFormatException e) { // TODO Jakub: handle exception System.err.println("Cannot parse man's workload '" + manWorkload + "'"); } } mans.put(manId, man); } } Element tasksE = getFirstChild(rootE, TASKS_E); if(tasksE != null) { for(Element taskE: Utils.getChilds(tasksE, TASK_E)) { String taskId = taskE.getAttribute(ID_A); String taskName = taskE.getAttribute(NAME_A); String taskManId = taskE.getAttribute(MAN_A); ManImpl man = mans.get(taskManId); // mans are already loaded if(man == null) { throw new WrongDataException("Task with id " + taskId + " is not assigned to any man"); } long taskStart = xmlTimeToTaskTime(taskE.getAttribute(START_A)); String durAttr = taskE.getAttribute(DURATION_A); long taskEffort; if(durAttr != null && durAttr.trim().length() > 0) { taskEffort = xmlDurationToTaskDuration(durAttr); } else { String endAttr = taskE.getAttribute(END_A); long taskEnd = xmlTimeToTaskTime(endAttr); // start with effort=1. Increase it until the real duration is over for(long newEffort = 2; true; newEffort++) { long tmpEndTime = Utils.countFinishTime(taskStart, newEffort, man.getWorkload()); // Note: we compare to (taskEnd+1), since the tasks start/end are counted mathematically. For example task with // duration 1 day starting on 2011-01-01 ends on 2011-01-02 (the second day) if(tmpEndTime > (taskEnd+1)) { // we are over, step back taskEffort = newEffort-1; break; } } } String usedStr = taskE.getAttribute(ACTUAL_A); long taskWorkedTime = 0; if(usedStr != null && usedStr.trim().length() > 0) { taskWorkedTime = xmlDurationToTaskDuration(taskE.getAttribute(ACTUAL_A)); } String bugId = taskE.getAttribute(BUGID_A); String colorTxt = taskE.getAttribute(COLOR_A); String comment = taskE.getAttribute(COMM_A); if(bugId != null && bugId.length() == 0) { bugId = null; } TaskImpl task = new TaskImpl(); task.setName(taskName); task.setStartTime(taskStart); task.setEffort(taskEffort); task.setWorkedTime(taskWorkedTime); task.setMan(man); task.setComment( comment ); task.setBugId(bugId); if(colorTxt != null && colorTxt.length() > 0) { int colorIndex = Integer.parseInt(colorTxt); if(colorIndex >= 0 && colorIndex < ColorLabel.COLOR_LABELS.length) { task.setColorLabel(ColorLabel.COLOR_LABELS[colorIndex]); } } // read predecessors ids Element predsE = getFirstChild(taskE, PREDECESSORS_E); if(predsE != null) { List<String> preds = new ArrayList<String>(); for(Element predE: Utils.getChilds(predsE, PREDECESSOR_E)) { preds.add(predE.getAttribute(PRED_A)); } taskPredecessorsIds.add(new Pair<TaskImpl, String[]>(task, preds.toArray(new String[preds.size()]))); } tasks.put(taskId, task); } } // now count the predecessors of tasks for(Pair<TaskImpl, String[]> taskAndPredIds: taskPredecessorsIds) { List<TaskImpl> preds = new ArrayList<TaskImpl>(); for(String predId: taskAndPredIds.snd) { TaskImpl pred = tasks.get(predId); if(pred == null) { System.out.println("Warning: Task predecessor with id " + predId + " doesn't exist"); // NOPMD by jakub on 6.8.09 15:50 } else if(pred == taskAndPredIds.fst) { System.out.println("Warning: Task with id " + predId + " is it's own predecessor"); // NOPMD by jakub on 6.8.09 15:50 } else { preds.add(pred); } } taskAndPredIds.fst.setPredecessors(preds.toArray(new TaskImpl[preds.size()])); } TaskModelImpl taskModel = new TaskModelImpl(tasks.values().toArray(new TaskImpl[tasks.size()]), mans.values().toArray(new ManImpl[mans.size()])); return taskModel; } catch (SAXException e) { throw new WrongDataException("Document is not valid data file", e); } catch (IOException e) { throw new WrongDataException("Can't read file: " + e.getMessage(), e); } catch (ParserConfigurationException e) { throw new WrongDataException("Document is not TaskBlocks project", e); } finally { if(input != null) { try { input.close(); } catch (IOException e) { throw new WrongDataException("Cannot close input stream: " + e.toString()); } } } } public void saveProject(URL url, TaskModelImpl model) throws TransformerException, ParserConfigurationException, IOException { if("file".equals(url.getProtocol())) { File f; try { f = new File(url.toURI()); } catch(URISyntaxException e) { f = new File(url.getPath()); } saveProject(f, model); } else if("http".equals(url.getProtocol())) { ByteArrayOutputStream tmp = new ByteArrayOutputStream(); saveProject(tmp, model); OutputStream out = null; HttpURLConnection con = null; try { con = (HttpURLConnection) url.openConnection(); con.setDoOutput(true); //con.setDoInput(false); con.setRequestMethod("PUT"); con.setRequestProperty("Content-Length", String.valueOf(tmp.size())); con.setRequestProperty("Content-Type", "application/xml"); con.connect(); out = con.getOutputStream(); out.write(tmp.toByteArray()); int responseCode = con.getResponseCode(); if(responseCode != 200) { throw new IOException("HTTP Error " + responseCode + ": " + con.getResponseMessage()); } } finally { if(out != null) { out.close(); } if(con != null) { con.disconnect(); } } } else { throw new IOException("Unsupported url protocol: " + url.getProtocol()); } } /** * Saves project to specified file * * @param f * @param model * @throws TransformerException * @throws ParserConfigurationException * @throws IOException */ public void saveProject(File f, TaskModelImpl model) throws TransformerException, ParserConfigurationException, IOException { FileOutputStream fos = new FileOutputStream(f); try { saveProject(fos, model); } finally { fos.close(); } } public void saveProject(OutputStream out, TaskModelImpl model) throws TransformerException, ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc; doc = dbf.newDocumentBuilder().newDocument(); _model = model; _taskIds = new HashMap<String, TaskImpl>(); _manIds = new HashMap<String, ManImpl>(); // build the xml tree saveProject(doc); prettyLayout((Element)doc.getFirstChild()); TransformerFactory tf = TransformerFactory.newInstance(); Transformer t = tf.newTransformer(); t.transform(new DOMSource(doc), new StreamResult(out)); } private void saveProject(Document doc) { Element rootE = doc.createElement(TASKMAN_E); doc.appendChild(rootE); Set<ManImpl> mans = new HashSet<ManImpl>(); // generate list of mans for(ManImpl m: _model._mans) { mans.add(m); } // generate task and man ids int lastTaskId = 1; int lastManId = 1; for(TaskImpl t: _model._tasks) { t._id = String.valueOf(lastTaskId++); } for(ManImpl man: mans) { man._id = String.valueOf(lastManId++); } // save mans Element mansE = doc.createElement(MANS_E); for(ManImpl man : mans) { saveMan(mansE, man); } rootE.appendChild(mansE); // save tasks Element tasksE = doc.createElement(TASKS_E); for(TaskImpl t: _model._tasks) { saveTask(tasksE, t); } rootE.setAttribute(VERSION_A, String.valueOf(CURRENT_VERSION)); rootE.appendChild(tasksE); } private void saveMan(Element mansE, ManImpl man) { Element manE = mansE.getOwnerDocument().createElement(MAN_E); manE.setAttribute(ID_A, man._id); manE.setAttribute(NAME_A, man.getName()); manE.setAttribute(WORKLOAD_A, String.valueOf((int)(man.getWorkload()*100))); mansE.appendChild(manE); } static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private static String taskTimeToXmlTime(long day) { return df.format(new Date(day * Utils.MILLISECONDS_PER_DAY + 8*60*60*1000)); } private static String taskDurationToXmlDuration(long dur) { return "P" + dur + "D"; } private static long xmlTimeToTaskTime(String time) throws WrongDataException { try { return Long.parseLong(time); } catch(NumberFormatException e) { // do nothing } try { return df.parse(time).getTime()/Utils.MILLISECONDS_PER_DAY; } catch (ParseException e) { // DO NOTHING } throw new WrongDataException("Wrong time value: " + time); } private static long xmlDurationToTaskDuration(String dur) throws WrongDataException{ if(dur == null || dur.trim().length() == 0) { return 0; } try { return Long.parseLong(dur); } catch(NumberFormatException e) { // do nothing } if(dur.startsWith("P") && dur.endsWith("D")) { try { return Long.parseLong(dur.substring(1,dur.length()-1)); } catch(NumberFormatException e) { // do nothing } } throw new WrongDataException("Wrong duration value: " + dur); } private void saveTask(Element tasksE, TaskImpl t) { Element taskE = tasksE.getOwnerDocument().createElement(TASK_E); taskE.setAttribute(NAME_A, t.getName()); taskE.setAttribute(ID_A, t._id); taskE.setAttribute(START_A, taskTimeToXmlTime(t.getStartTime())); taskE.setAttribute(DURATION_A, taskDurationToXmlDuration(t.getEffort())); if(t.getWorkedTime() != 0) { taskE.setAttribute(ACTUAL_A, taskDurationToXmlDuration(t.getWorkedTime())); } taskE.setAttribute(MAN_A, t.getMan()._id); taskE.setAttribute(COMM_A, t.getComment()); if(t.getColorLabel() != null) { taskE.setAttribute(COLOR_A, String.valueOf(t.getColorLabel()._index)); } if(t.getBugId() != null && t.getBugId().trim().length() > 0) { taskE.setAttribute(BUGID_A, t.getBugId().trim()); } // save predecessors if(t.getPredecessors().length > 0) { Element predsE = taskE.getOwnerDocument().createElement(PREDECESSORS_E); for(TaskImpl pred: t.getPredecessors()) { Element predE = predsE.getOwnerDocument().createElement(PREDECESSOR_E); predE.setAttribute(PRED_A, pred._id); predsE.appendChild(predE); } taskE.appendChild(predsE); } tasksE.appendChild(taskE); } private Element getFirstChild(Element e, String name) { NodeList nl = e.getChildNodes(); for(int i = 0; i < nl.getLength(); i++) { Node n = nl.item(i); if(n.getNodeType() == Node.ELEMENT_NODE && name.equals(n.getNodeName())) { return (Element)n; } } return null; } private void prettyLayout(Element e) { prettyLayoutRec(e, ""); } private void prettyLayoutRec(Element e, String currentIndent) { // insert spaces before 'e' // but only if indent > 0. This also resolves problem that we cannot insert // anything in Document node (before root element). if(currentIndent.length() > 0) { e.getParentNode().insertBefore(e.getOwnerDocument().createTextNode(currentIndent), e); } // first check if element has some sub-element. if true, prettyLayout them // recursively with increase indent NodeList nl = e.getChildNodes(); boolean hasChildrenElems = false; for(int i = 0; i < nl.getLength(); i++) { Node n = nl.item(i); if(n.getNodeType() == Node.ELEMENT_NODE) { hasChildrenElems = true; break; } } if(hasChildrenElems) { // \n after start-tag. it means before first child e.insertBefore(e.getOwnerDocument().createTextNode("\n"), e.getFirstChild()); // indent before end-tag. It means just as last child e.appendChild(e.getOwnerDocument().createTextNode(currentIndent)); // we must get the nodelist again, because previous adding of childs broked // the old nodelist. Node n = e.getFirstChild(); while(n != null) { if(n.getNodeType() == Node.ELEMENT_NODE) { prettyLayoutRec((Element)n, currentIndent + " "); } n = n.getNextSibling(); } } // \n after end-tag Node text = e.getOwnerDocument().createTextNode("\n"); if(e.getNextSibling() == null) { if(e.getParentNode().getNodeType() != Node.DOCUMENT_NODE) { e.getParentNode().appendChild(text); } } else { e.getParentNode().insertBefore(text, e.getNextSibling()); } } }