Java tutorial
/* * Copyright 2006 The National Library of New Zealand * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.webcurator.ui.tools.controller; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.propertyeditors.CustomBooleanEditor; import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.validation.BindException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.commons.CommonsMultipartFile; import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractCommandController; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.webcurator.core.harvester.coordinator.HarvestCoordinator; import org.webcurator.core.harvester.coordinator.HarvestLogManager; import org.webcurator.core.scheduler.TargetInstanceManager; import org.webcurator.core.store.tools.HarvestResourceNodeTreeBuilder; import org.webcurator.core.store.tools.QualityReviewFacade; import org.webcurator.core.store.tools.WCTNode; import org.webcurator.core.store.tools.WCTNodeTree; import org.webcurator.core.util.XMLConverter; import org.webcurator.domain.model.core.HarvestResource; import org.webcurator.domain.model.core.HarvestResourceDTO; import org.webcurator.domain.model.core.HarvestResult; import org.webcurator.domain.model.core.TargetInstance; import org.webcurator.ui.common.Constants; import org.webcurator.ui.tools.command.TreeToolCommand; import org.xml.sax.SAXException; /** * The TreeToolController is responsible for rendering the * harvest web site as a tree structure. * @author bbeaumont */ public class TreeToolController extends AbstractCommandController { public class AQAElement { private String url = ""; private String contentFile = ""; private String contentType = ""; private long contentLength = 0L; private AQAElement(String url, String contentFile, String contentType, long contentLength) { this.url = url; this.contentFile = contentFile; this.contentType = contentType; this.contentLength = contentLength; } public String getUrl() { return url; } public String getContentFile() { return contentFile; } public String getContentType() { return contentType; } public long getContentLength() { return contentLength; } } private static Log log = LogFactory.getLog(TreeToolController.class); private QualityReviewFacade qualityReviewFacade = null; private String successView = "TreeTool"; private HarvestResourceUrlMapper harvestResourceUrlMapper; private boolean enableAccessTool = false; private String uploadedFilesDir; /** Automated QA Url */ private String autoQAUrl = ""; /** the name of the content directory. */ public static final String DIR_CONTENT = "content"; HarvestLogManager harvestLogManager; TargetInstanceManager targetInstanceManager; public TreeToolController() { super.setCommandClass(TreeToolCommand.class); } @Override public void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { // Determine the necessary formats. NumberFormat nf = NumberFormat.getInstance(request.getLocale()); // Register the binders. binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, nf, true)); binder.registerCustomEditor(Boolean.class, "propagateDelete", new CustomBooleanEditor(true)); // to actually be able to convert Multipart instance to byte[] // we have to register a custom editor (in this case the // ByteArrayMultipartEditor binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor()); // now Spring knows how to handle multipart object and convert them } @SuppressWarnings("unchecked") @Override protected ModelAndView handle(HttpServletRequest req, HttpServletResponse res, Object comm, BindException errors) throws Exception { TreeToolCommand command = (TreeToolCommand) comm; TargetInstance ti = (TargetInstance) req.getSession().getAttribute("sessionTargetInstance"); // If the tree is not loaded then load the tree into session // data and then load any AQA 'importable items' into session data. if (command.getLoadTree() != null) { // load the tree.. log.info("Generating Tree"); HarvestResourceNodeTreeBuilder treeBuilder = qualityReviewFacade .getHarvestResultTree(command.getLoadTree()); WCTNodeTree tree = treeBuilder.getTree(); req.getSession().setAttribute("tree", tree); command.setHrOid(command.getLoadTree()); log.info("Tree complete"); if (autoQAUrl != null && autoQAUrl.length() > 0) { List<AQAElement> candidateElements = new ArrayList<AQAElement>(); // load AQA 'importable items' (if any).. File xmlFile; try { xmlFile = harvestLogManager.getLogfile(ti, command.getLogFileName()); } catch (Exception e) { xmlFile = null; log.info("Missing AQA report file: " + command.getLogFileName()); } if (xmlFile != null) { Document aqaResult = readXMLDocument(xmlFile); NodeList parentElementsNode = aqaResult.getElementsByTagName("missingElements"); if (parentElementsNode.getLength() > 0) { NodeList elementNodes = ((Element) parentElementsNode.item(0)) .getElementsByTagName("element"); for (int i = 0; i < elementNodes.getLength(); i++) { Element element = (Element) elementNodes.item(i); if (element.getAttribute("statuscode").equals("200")) { candidateElements.add(new AQAElement(element.getAttribute("url"), element.getAttribute("contentfile"), element.getAttribute("contentType"), Long.parseLong(element.getAttribute("contentLength")))); } } } req.getSession().setAttribute("aqaImports", candidateElements); } } } // Load the tree items from the session. WCTNodeTree tree = (WCTNodeTree) req.getSession().getAttribute("tree"); List<AQAElement> imports = (List<AQAElement>) req.getSession().getAttribute("aqaImports"); // Go back to the page if there were validation errors. if (errors.hasErrors()) { ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("command", command); mav.addObject("aqaImports", imports); mav.addObject(Constants.GBL_ERRORS, errors); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } // Handle any tree actions. else if (command.isAction(TreeToolCommand.ACTION_TREE_ACTION)) { // If we are toggling things open/closed.. if (command.getToggleId() != null) { tree.toggle(command.getToggleId()); } // if we're pruning.. if (command.getMarkForDelete() != null) { tree.markForDelete(command.getMarkForDelete(), command.getPropagateDelete()); } // if we're importing single items.. if (command.getTargetURL() != null) { HarvestResourceNodeTreeBuilder tb = new HarvestResourceNodeTreeBuilder(); try { URL parentUrl = tb.getParent(new URL(command.getTargetURL())); } catch (MalformedURLException me) { errors.reject("tools.errors.invalidtargeturl"); ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("command", command); mav.addObject("aqaImports", imports); mav.addObject(Constants.GBL_ERRORS, errors); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } if (command.getImportType().equals(TreeToolCommand.IMPORT_FILE)) { // We're importing a file from the user's file system, uploaded // via their browser. We need to store the file so it can be added // to the archive when the SAVE command is eventually issued. // We also need to add a node to the tree-view in such a way that the // user can distinguish imported files from pruned files and // original files. MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req; CommonsMultipartFile uploadedFile = (CommonsMultipartFile) multipartRequest .getFile("sourceFile"); // save uploaded file as tempFileName in configured uploadedFilesDir String tempFileName = UUID.randomUUID().toString(); File xfrFile = new File(uploadedFilesDir + tempFileName); StringBuffer buf = new StringBuffer(); buf.append("HTTP/1.1 200 OK\n"); buf.append("Content-Type: "); buf.append(uploadedFile.getContentType() + "\n"); buf.append("Content-Length: "); buf.append(uploadedFile.getSize() + "\n"); buf.append("Connection: close\n\n"); FileOutputStream fos = new FileOutputStream(xfrFile); fos.write(buf.toString().getBytes()); fos.write(uploadedFile.getBytes()); fos.close(); tree.insert(command.getTargetURL(), xfrFile.length(), tempFileName, uploadedFile.getContentType()); } if (command.getImportType().equals(TreeToolCommand.IMPORT_URL)) { // We're importing a file via a URL that the user has specified, we // need to use the URL to do a HTTP GET and store the resultant // down-loaded file so it can be added to the archive when the // SAVE command is eventually issued. // We also need to add a node to the tree-view in such a way that the // user can distinguish imported files from pruned files and // original files. // save uploaded file as tempFileName in configured uploadedFilesDir String tempFileName = UUID.randomUUID().toString(); File xfrFile = new File(uploadedFilesDir + tempFileName); FileOutputStream fos = new FileOutputStream(xfrFile); //String contentType = null; String outStrings[] = new String[1]; try { fos.write(fetchHTTPResponse(command.getSourceURL(), outStrings)); } catch (HTTPGetException ge) { errors.reject("tools.errors.httpgeterror", new Object[] { command.getSourceURL(), ge.getMessage() }, ""); ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("aqaImports", imports); mav.addObject("command", command); mav.addObject(Constants.GBL_ERRORS, errors); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } finally { fos.close(); } tree.insert(command.getTargetURL(), xfrFile.length(), tempFileName, outStrings[0]); } } ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("aqaImports", imports); mav.addObject("command", command); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } // Handle browse action. else if (command.isAction(TreeToolCommand.ACTION_VIEW)) { HarvestResource resource = tree.getNodeCache().get(command.getSelectedRow()).getSubject(); Long resultOid = resource.getResult().getOid(); String url = resource.getName(); if (enableAccessTool && harvestResourceUrlMapper != null) { return new ModelAndView("redirect:" + harvestResourceUrlMapper.generateUrl(resource.getResult(), resource.buildDTO())); } else { return new ModelAndView("redirect:/curator/tools/browse/" + resultOid + "/" + url); } } // Handle show hop path action. else if (command.isAction(TreeToolCommand.ACTION_SHOW_HOP_PATH)) { TargetInstance tinst = (TargetInstance) req.getSession().getAttribute("sessionTargetInstance"); Long instanceOid = tinst.getOid(); String url = command.getSelectedUrl(); return new ModelAndView("redirect:/curator/target/show-hop-path.html?targetInstanceOid=" + instanceOid + "&logFileName=sortedcrawl.log&url=" + url); } // handle import of one or more AQA items.. else if (command.isAction(TreeToolCommand.IMPORT_AQA_FILE)) { // iterate over the selected (checked) items.. if (command.getAqaImports() != null) { List<AQAElement> removeElements = new ArrayList<AQAElement>(); String[] aqaImportUrls = command.getAqaImports(); for (int i = 0; i < aqaImportUrls.length; i++) { String aqaUrl = aqaImportUrls[i]; for (Iterator iter = imports.iterator(); iter.hasNext();) { AQAElement elem = (AQAElement) iter.next(); if (elem.getUrl().equals(aqaUrl)) { // We're importing a missing file, captured by the AQA process. // We need to store the file with HTTP header info so it can be added // to the archive when the SAVE command is eventually issued. // We also need to add a node to the tree-view in such a way that the // user can distinguish imported files from pruned files and // original files. File aqaFile = null; try { aqaFile = harvestLogManager.getLogfile(ti, elem.getContentFile()); // save imported file using a random tempFileName in configured uploadedFilesDir String tempFileName = UUID.randomUUID().toString(); File xfrFile = new File(uploadedFilesDir + tempFileName); StringBuffer buf = new StringBuffer(); buf.append("HTTP/1.1 200 OK\n"); buf.append("Content-Type: "); buf.append(elem.getContentType() + "\n"); buf.append("Content-Length: "); buf.append(elem.getContentLength() + "\n"); buf.append("Connection: close\n\n"); FileOutputStream fos = new FileOutputStream(xfrFile); fos.write(buf.toString().getBytes()); FileInputStream fin = new FileInputStream(aqaFile); byte[] bytes = new byte[8192]; int len = 0; while ((len = fin.read(bytes)) >= 0) { fos.write(bytes, 0, len); } fos.close(); fin.close(); tree.insert(aqaUrl, xfrFile.length(), tempFileName, elem.getContentType()); removeElements.add(elem); } catch (Exception e) { log.info("Missing AQA import file: " + elem.getContentFile()); } } } } ; //end for for (Iterator remit = removeElements.iterator(); remit.hasNext();) { AQAElement rem = (AQAElement) remit.next(); imports.remove(rem); } } ; ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("aqaImports", imports); mav.addObject("command", command); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } // Handle the save action. else if (command.isAction(TreeToolCommand.ACTION_SAVE)) { List<String> uris = new LinkedList<String>(); for (WCTNode node : tree.getPrunedNodes()) { if (node.getSubject() != null) { uris.add(node.getSubject().getName()); } } List<HarvestResourceDTO> hrs = new LinkedList<HarvestResourceDTO>(); for (HarvestResourceDTO dto : tree.getImportedNodes()) { hrs.add(dto); } HarvestResult result = qualityReviewFacade.copyAndPrune(command.getHrOid(), uris, hrs, command.getProvenanceNote(), tree.getModificationNotes()); // Make sure that the tree is removed from memory. removeTree(req); removeAQAImports(req); return new ModelAndView("redirect:/" + Constants.CNTRL_TI + "?targetInstanceId=" + result.getTargetInstance().getOid() + "&cmd=edit&init_tab=RESULTS"); } else if (command.isAction(TreeToolCommand.ACTION_CANCEL)) { // Make sure that objects removed from memory. removeTree(req); removeAQAImports(req); TargetInstance tinst = (TargetInstance) req.getSession().getAttribute("sessionTargetInstance"); return new ModelAndView("redirect:/curator/target/quality-review-toc.html?targetInstanceOid=" + tinst.getOid() + "&harvestResultId=" + command.getHrOid()); } // Handle an unknown action? else { ModelAndView mav = new ModelAndView(getSuccessView()); mav.addObject("tree", tree); mav.addObject("command", command); mav.addObject("aqaImports", imports); mav.addObject(Constants.GBL_ERRORS, errors); if (autoQAUrl != null && autoQAUrl.length() > 0) { mav.addObject("showAQAOption", 1); } else { mav.addObject("showAQAOption", 0); } return mav; } } public class HTTPGetException extends RuntimeException { private static final long serialVersionUID = -82352605532244805L; private String message = ""; protected HTTPGetException(String message) { super(); this.message = message; } protected HTTPGetException(String message, Exception cause) { super(cause); this.message = message; } @Override public String getMessage() { return message; } } private byte[] fetchHTTPResponse(String url, String[] outStrings) { GetMethod getMethod = new GetMethod(url); HttpClient client = new HttpClient(); try { int result = client.executeMethod(getMethod); if (result != HttpURLConnection.HTTP_OK) { throw new HTTPGetException("HTTP GET Status=" + result); } Header[] headers = getMethod.getResponseHeaders(); StringBuffer buf = new StringBuffer(); buf.append("HTTP/1.1 200 OK\n"); for (int i = 0; i < headers.length; i++) { buf.append(headers[i].getName() + ": "); buf.append(headers[i].getValue() + "\n"); if (headers[i].getName().equalsIgnoreCase("content-type")) { outStrings[0] = headers[i].getValue(); } } buf.append("\n"); ByteArrayOutputStream os = new ByteArrayOutputStream(); os.write(buf.toString().getBytes()); os.write(getMethod.getResponseBody()); return os.toByteArray(); } catch (HTTPGetException je) { throw je; } catch (Exception e) { throw new HTTPGetException("Unable to fetch content at " + url + ".", e); } finally { getMethod.releaseConnection(); } } private void removeTree(HttpServletRequest req) { WCTNodeTree tree = (WCTNodeTree) req.getSession().getAttribute("tree"); tree.clear(); req.getSession().removeAttribute("tree"); System.gc(); } private void removeAQAImports(HttpServletRequest req) { req.getSession().removeAttribute("aqaImports"); System.gc(); } private Document readXMLDocument(File f) throws SAXException, IOException, ParserConfigurationException { StringBuffer sb = new StringBuffer(); BufferedReader in = new BufferedReader(new FileReader(f)); try { String str; while ((str = in.readLine()) != null) { sb.append(str); } } finally { in.close(); } return XMLConverter.StringToDocument(sb.toString()); } public void setQualityReviewFacade(QualityReviewFacade qrf) { this.qualityReviewFacade = qrf; } public String getSuccessView() { return successView; } public void setSuccessView(String successView) { this.successView = successView; } public HarvestResourceUrlMapper getHarvestResourceUrlMapper() { return harvestResourceUrlMapper; } public void setHarvestResourceUrlMapper(HarvestResourceUrlMapper harvestResourceUrlMapper) { this.harvestResourceUrlMapper = harvestResourceUrlMapper; } public void setEnableAccessTool(boolean enableAccessTool) { this.enableAccessTool = enableAccessTool; } public boolean isEnableAccessTool() { return enableAccessTool; } public String getUploadedFilesDir() { return uploadedFilesDir; } public void setUploadedFilesDir(String uploadedFilesDir) { this.uploadedFilesDir = uploadedFilesDir; } public void setAutoQAUrl(String autoQAUrl) { this.autoQAUrl = autoQAUrl; } public String getAutoQAUrl() { return autoQAUrl; } /** * @param harvestLogManager the harvestLogManager to set */ public void setHarvestLogManager(HarvestLogManager harvestLogManager) { this.harvestLogManager = harvestLogManager; } /** * @param targetInstanceManager the targetInstanceManager to set */ public void setTargetInstanceManager(TargetInstanceManager targetInstanceManager) { this.targetInstanceManager = targetInstanceManager; } }