/******************************************************************************* * Copyright 2013 * Ubiquitous Knowledge Processing (UKP) Lab * Technische Universität Darmstadt * * 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 de.tudarmstadt.ukp.csniper.webapp.search.page; import static java.util.Arrays.asList; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow; import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder; import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn; import org.apache.wicket.markup.html.link.DownloadLink; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.IChoiceRenderer; import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.request.handler.resource.*; import org.apache.wicket.request.resource.ContextRelativeResource; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.resource.*; import org.springframework.dao.NonTransientDataAccessException; import org.springframework.security.core.context.SecurityContextHolder; import de.tudarmstadt.ukp.csniper.webapp.analysis.ParseTreeResource; import de.tudarmstadt.ukp.csniper.webapp.analysis.uima.ParsingPipeline; import de.tudarmstadt.ukp.csniper.webapp.evaluation.EvaluationRepository; import de.tudarmstadt.ukp.csniper.webapp.evaluation.SortableEvaluationResultDataProvider; import de.tudarmstadt.ukp.csniper.webapp.evaluation.SortableEvaluationResultDataProvider.ResultFilter; import de.tudarmstadt.ukp.csniper.webapp.evaluation.model.CachedParse; import de.tudarmstadt.ukp.csniper.webapp.evaluation.model.EvaluationItem; import de.tudarmstadt.ukp.csniper.webapp.evaluation.model.EvaluationResult; import de.tudarmstadt.ukp.csniper.webapp.evaluation.page.ContextView; import de.tudarmstadt.ukp.csniper.webapp.page.ApplicationPageBase; import de.tudarmstadt.ukp.csniper.webapp.project.ProjectRepository; import de.tudarmstadt.ukp.csniper.webapp.search.ContextProvider; import de.tudarmstadt.ukp.csniper.webapp.search.CorpusService; import de.tudarmstadt.ukp.csniper.webapp.search.PreparedQuery; import de.tudarmstadt.ukp.csniper.webapp.search.SearchEngine; import de.tudarmstadt.ukp.csniper.webapp.support.uima.CasHolder; import de.tudarmstadt.ukp.csniper.webapp.support.wicket.AnalysisPanel; import de.tudarmstadt.ukp.csniper.webapp.support.wicket.CustomDataTable; import de.tudarmstadt.ukp.csniper.webapp.support.wicket.EmbeddableImage; import de.tudarmstadt.ukp.csniper.webapp.support.wicket.ExtendedIndicatingAjaxButton; /** * Evaluation Page */ @SuppressWarnings({ "rawtypes", "unused" }) public class SearchPage extends ApplicationPageBase { private static final long serialVersionUID = 1L; private final Log log = LogFactory.getLog(getClass()); private List<EvaluationItem> items; private QueryForm queryForm; private LimitForm limitForm; private WebMarkupContainer contextViewsContainer; private ListView<ContextView> contextViews; private List<IColumn<EvaluationResult, String>> columns; private Component resultTable; private ModalWindow analysisModal; private ParsingPipeline pp; private SortableEvaluationResultDataProvider dataProvider; private boolean contextAvailable = false; private static final int ROWS_PER_PAGE = 10; private static final int MAX_RESULTS = 1000; // TODO: Make parameterizable via web interface. private static final int MAX_DOWNLOAD_RESULTS = 5000; private static final int MIN_ITEMS_ANNOTATED = 10; @SpringBean(name = "evaluationRepository") private EvaluationRepository repository; @SpringBean(name = "projectRepository") private ProjectRepository projectRepository; @SpringBean(name = "corpusService") private CorpusService corpusService; @SpringBean(name = "contextProvider") private ContextProvider contextProvider; private class QueryForm extends Form<QueryFormModel> { private static final long serialVersionUID = 1L; final TextArea<String> queryInput; final ExtendedIndicatingAjaxButton queryButton; final DropDownChoice<String> collectionIdInput; final DropDownChoice<SearchEngine> engineInput; @SuppressWarnings({ "serial" }) public QueryForm(String id) { super(id, new CompoundPropertyModel<QueryFormModel>(new QueryFormModel())); // Tab contents queryInput = (TextArea<String>) new TextArea<String>("query").setRequired(true); // submit button queryButton = new ExtendedIndicatingAjaxButton("queryButton", new Model<String>( "Submit query"), new Model<String>("Running query ...")) { @Override public void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { QueryFormModel model = QueryForm.this.getModelObject(); String user = SecurityContextHolder.getContext().getAuthentication().getName(); PreparedQuery query = null; try { query = model.engine.createQuery("", model.collectionId, model.query); query.setMaxResults(MAX_RESULTS); items = query.execute(); int resultCount = query.size(); limitForm.setResultCount(resultCount); // new results: show limitForm if too many results, show saveButton limitForm.setVisible(resultCount > MAX_RESULTS); } catch (NonTransientDataAccessException e) { error("Error executing query " + model.query + ": " + e.getMessage()); items = new ArrayList<EvaluationItem>(); // error -> no results: hide limitForm, saveButton limitForm.setVisible(false); } finally { // IOUtils.closeQuietly(query); } // new items (or error), so show only item columns, no filters // update dataprovider dataProvider = new SortableEvaluationResultDataProvider( createEvaluationResults(items)); dataProvider.setSort("item.documentId", SortOrder.ASCENDING); dataProvider.setFilter(ResultFilter.ALL); // then update the table resultTable = resultTable.replaceWith(new CustomDataTable<EvaluationResult>( "resultTable", columns, dataProvider, ROWS_PER_PAGE)); contextAvailable = true; updateComponents(aTarget); } @Override public void onError(AjaxRequestTarget aTarget, Form<?> aForm) { super.onError(aTarget, aForm); aTarget.add(getFeedbackPanel()); } }; add(queryInput); add(queryButton); // Add download functionality for query result. IModel<File> fileModel = new AbstractReadOnlyModel<File>() { @Override public File getObject() { File rvalfile = null; log.info("Downloadlink ... generating rval file."); if(items!=null) { if(items.size() > 0) { ServletContext ctx = WebApplication.get().getServletContext(); final String path = ctx.getRealPath("/"); PrintWriter pw = null; try { pw = new PrintWriter(new File(path + "/results.txt")); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } // Add all query result items to rval list. for(int i = 0; i < items.size(); i++) { if(i < MAX_DOWNLOAD_RESULTS) { pw.write(items.get(i) + "\n"); } else { break; } } pw.close(); rvalfile = new File(path + "/results.txt"); } } else { log.warn("Nothing to be saved."); } return rvalfile; } }; DownloadLink dynamicDownloadlink = new DownloadLink("downloadButton", fileModel) { @Override public void onClick() { File file = (File)getModelObject(); IResourceStream resourceStream = new FileResourceStream(new org.apache.wicket.util.file.File(file)); getRequestCycle().scheduleRequestHandlerAfterCurrent(new ResourceStreamRequestHandler(resourceStream, file.getName())); } }; add(dynamicDownloadlink); // collection dropdown collectionIdInput = new DropDownChoice<String>("collectionId", new LoadableDetachableModel<List<String>>() { @Override protected List<String> load() { return corpusService.listCorpora(); } }, new IChoiceRenderer<String>() { @Override public Object getDisplayValue(String aObject) { return corpusService.getCorpus(aObject).getName(); } @Override public String getIdValue(String aObject, int aIndex) { return corpusService.getCorpus(aObject).getId(); } }) { @Override protected boolean wantOnSelectionChangedNotifications() { return true; } @Override protected void onSelectionChanged(String aNewSelection) { engineInput.setModelObject(null); queryInput.setModelObject(""); } }; add(collectionIdInput); // engine dropdown engineInput = (DropDownChoice<SearchEngine>) new DropDownChoice<SearchEngine>("engine", new LoadableDetachableModel<List<SearchEngine>>() { @Override protected List<SearchEngine> load() { QueryFormModel model = QueryForm.this.getModelObject(); if (model.collectionId != null) { return corpusService.listEngines(model.collectionId); } else { return new ArrayList<SearchEngine>(); } } }, new IChoiceRenderer<SearchEngine>() { @Override public Object getDisplayValue(SearchEngine aObject) { return aObject.getName(); } @Override public String getIdValue(SearchEngine aObject, int aIndex) { return aObject.getName(); } }) { @Override protected boolean wantOnSelectionChangedNotifications() { return true; } @Override protected void onSelectionChanged(SearchEngine aEngine) { queryInput.setModelObject(""); } }.setRequired(true); add(engineInput); } } private static class QueryFormModel implements Serializable { private static final long serialVersionUID = 1L; private String query = ""; private boolean randomize = false; private String collectionId = null; private SearchEngine engine = null; } private class LimitForm extends Form { private static final long serialVersionUID = 1L; private int resultLimit = 0; private Label limitLabel; public LimitForm(String id) { super(id); add(limitLabel = new Label("limitLabel", "")); add(new TextField<Integer>("limitInput", new PropertyModel<Integer>(this, "resultLimit"))); add(new ExtendedIndicatingAjaxButton("limitButton", new Model<String>("Show results"), new Model<String>("Retrieving results")) { private static final long serialVersionUID = 1L; @Override protected void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { QueryFormModel model = queryForm.getModelObject(); PreparedQuery query = null; try { query = model.engine.createQuery("", model.collectionId, model.query); query.setMaxResults(resultLimit); items = query.execute(); } catch (NonTransientDataAccessException e) { error("Error executing query " + model.query + ": " + e.getMessage()); items = new ArrayList<EvaluationItem>(); } finally { // IOUtils.closeQuietly(query); } // query results: show saveButton, hide result columns and filter options limitForm.setVisible(false); // update dataprovider dataProvider = new SortableEvaluationResultDataProvider( createEvaluationResults(items)); dataProvider.setSort("item.documentId", SortOrder.ASCENDING); dataProvider.setFilter(ResultFilter.ALL); // then update the table resultTable = resultTable.replaceWith(new CustomDataTable<EvaluationResult>( "resultTable", columns, dataProvider, ROWS_PER_PAGE)); updateComponents(aTarget); } @Override public void onError(AjaxRequestTarget aTarget, Form<?> aForm) { super.onError(aTarget, aForm); // Make sure the feedback messages are rendered aTarget.add(getFeedbackPanel()); } }); } public void setResultCount(int aResultCount) { resultLimit = aResultCount; limitLabel.setDefaultModelObject("There are " + aResultCount + " results in total. How many results shall be shown? "); } } /** * Constructor that is invoked when page is invoked without a session. */ @SuppressWarnings({ "serial" }) public SearchPage() { contextViewsContainer = new WebMarkupContainer("contextViewsContainer") { { contextViews = new ListView<ContextView>("contextViews") { @Override protected void populateItem(ListItem aItem) { aItem.add((Component) aItem.getModelObject()); } }; add(contextViews); } }; contextViewsContainer.setOutputMarkupId(true); add(contextViewsContainer); columns = new ArrayList<IColumn<EvaluationResult, String>>(); columns.add(new AbstractColumn<EvaluationResult, String>(new Model<String>("")) { @Override public void populateItem(final Item<ICellPopulator<EvaluationResult>> aCellItem, String aComponentId, final IModel<EvaluationResult> model) { EmbeddableImage iconContext = new EmbeddableImage(aComponentId, new ContextRelativeResource("images/context.png")); iconContext.add(new AjaxEventBehavior("onclick") { @Override protected void onEvent(AjaxRequestTarget aTarget) { try { contextViews.setList(asList(new ContextView(contextProvider, model .getObject().getItem()))); aTarget.add(contextViewsContainer); } catch (IOException e) { error("Unable to load context: " + e.getMessage()); aTarget.add(getFeedbackPanel()); } } }); iconContext.add(new AttributeModifier("class", new Model<String>("clickableElement"))); aCellItem.add(iconContext); } }); columns.add(new AbstractColumn<EvaluationResult, String>(new Model<String>("")) { @Override public void populateItem(final Item<ICellPopulator<EvaluationResult>> aCellItem, String aComponentId, final IModel<EvaluationResult> model) { EmbeddableImage iconAnalysis = new EmbeddableImage(aComponentId, new ContextRelativeResource("images/analysis.png")); iconAnalysis.add(new AjaxEventBehavior("onclick") { @Override protected void onEvent(AjaxRequestTarget aTarget) { EvaluationItem item = model.getObject().getItem(); CachedParse cachedTree = repository.getCachedParse(item); ParseTreeResource ptr; if (cachedTree != null) { ptr = new ParseTreeResource(cachedTree.getPennTree()); } else { if (pp == null) { pp = new ParsingPipeline(); } CasHolder ch = new CasHolder(pp.parseInput("stanfordParser", corpusService.getCorpus(item.getCollectionId()).getLanguage(), item.getCoveredText())); ptr = new ParseTreeResource(ch); } analysisModal.setContent(new AnalysisPanel(analysisModal.getContentId(), ptr)); analysisModal.show(aTarget); } }); iconAnalysis.add(new AttributeModifier("class", new Model<String>( "clickableElement"))); aCellItem.add(iconAnalysis); } }); columns.add(new PropertyColumn<EvaluationResult, String>(new Model<String>("Doc"), "item.documentId", "item.documentId")); columns.add(new PropertyColumn<EvaluationResult, String>(new Model<String>("Left"), "item.leftContext", "item.leftContext") { @Override public String getCssClass() { return contextAvailable ? "leftContext" : " hideCol"; } }); columns.add(new PropertyColumn<EvaluationResult, String>(new Model<String>("Match"), "item.match", "item.match") { @Override public String getCssClass() { return contextAvailable ? "match nowrap" : null; } }); columns.add(new PropertyColumn<EvaluationResult, String>(new Model<String>("Right"), "item.rightContext", "item.rightContext") { @Override public String getCssClass() { return contextAvailable ? "rightContext" : " hideCol"; } }); add(queryForm = new QueryForm("queryForm")); add(limitForm = (LimitForm) new LimitForm("limit").setOutputMarkupPlaceholderTag(true)); add(resultTable = new Label("resultTable").setOutputMarkupId(true)); add(analysisModal = new ModalWindow("analysisModal")); analysisModal.setTitle("Parse tree"); analysisModal.setInitialWidth(65 * 16); analysisModal.setInitialHeight(65 * 9); // autosize does not work... // analysisModal.setAutoSize(true); // at start, don't show: save button, results columns, filter limitForm.setVisible(false); } private List<EvaluationResult> createEvaluationResults(List<EvaluationItem> aItems) { String user = SecurityContextHolder.getContext().getAuthentication().getName(); Set<EvaluationResult> results = new HashSet<EvaluationResult>(); for (EvaluationItem item : aItems) { results.add(new EvaluationResult(item, user, "")); } return new ArrayList<EvaluationResult>(results); } private void updateComponents(AjaxRequestTarget aTarget) { aTarget.add(getFeedbackPanel(), limitForm, resultTable); } }