/*******************************************************************************
* 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);
}
}