/************************************************************************************************** * Copyright (c) 2010 Fabian Steeg. All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 which accompanies this * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * <p/> * Contributors: Fabian Steeg - initial API and implementation *************************************************************************************************/ package de.uni_koeln.ub.drc.ui.views; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.annotation.PostConstruct; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.part.ViewPart; import scala.Enumeration; import scala.collection.JavaConversions; import com.quui.sinist.XmlDb; import de.uni_koeln.ub.drc.data.Index; import de.uni_koeln.ub.drc.data.Page; import de.uni_koeln.ub.drc.data.SearchOption; import de.uni_koeln.ub.drc.data.Status; import de.uni_koeln.ub.drc.data.Tag; import de.uni_koeln.ub.drc.data.Word; import de.uni_koeln.ub.drc.ui.DrcUiActivator; import de.uni_koeln.ub.drc.ui.Messages; import de.uni_koeln.ub.drc.util.Chapter; import de.uni_koeln.ub.drc.util.Count; import de.uni_koeln.ub.drc.util.MetsTransformer; /** * View containing a search field and a table viewer displaying pages. * * @author Fabian Steeg (fsteeg), Mihail Atanassov (matana) */ public final class SearchView extends ViewPart { /** * The class / SearchView ID */ public static final String ID = SearchView.class.getName().toLowerCase(); private Text searchField; private Text tagField; private Label resultCount; private TreeViewer viewer; private Combo searchOptions; private TreeMap<String, Enumeration.Value> options = new TreeMap<String, Enumeration.Value>(); private Combo volumes; private Composite searchComposite; private Button close; private List<String> allPages; private int index; private Label currentPageLabel; private List<Page> toExport; private String selected; /** * @param parent * The parent composite for this part */ @Override public void createPartControl(Composite parent) { getSite().setSelectionProvider(viewer); searchComposite = new Composite(parent, SWT.NONE); searchComposite.setLayout(new GridLayout(7, false)); initVolumeSelector(searchComposite); initSearchField(searchComposite); initOptionsCombo(searchComposite); initTableViewer(parent); addPageInfoBar(parent); GridLayoutFactory.fillDefaults().generateLayout(parent); } @Override public void setFocus() { } /** * @return The selection to export */ public List<Page> getSelectedPages() { return toExport; } /** * Select the last word edited by this user. */ public void select() { String latestPage = DrcUiActivator.getDefault().currentUser() .latestPage(); if (latestPage.trim().isEmpty()) { return; } String volume = latestPage.split("_")[1].split("-")[0]; //$NON-NLS-1$ //$NON-NLS-2$ volumes.select(Index.RF().indexOf(volume)); setInput(); if (viewer.getTree().getItems().length == 0) { throw new IllegalArgumentException(Messages.get().NoEntries); } allPages = new ArrayList<String>( JavaConversions.asJavaList(content.modelIndex.pages())); Collections.sort(allPages); for (String pageId : allPages) { if (pageId.equals(latestPage)) { select(pageId); break; } } if (viewer.getSelection().isEmpty()) { viewer.setSelection(new StructuredSelection(allPages.get(0))); } } /** * Updates the TreeViewer after a Word has been modified and saved. */ public void updateTreeViewer() { viewer.setLabelProvider(new SearchViewLabelProvider()); } private Comparator<Object> comp = new Comparator<Object>() { @Override public int compare(Object p1, Object p2) { if (p1 instanceof Page && p2 instanceof Page) return ((Page) p1).id().compareTo(((Page) p2).id()); return p1.toString().compareTo(p2.toString()); } }; private Combo show; private void initVolumeSelector(Composite searchComposite) { Label label1 = new Label(searchComposite, SWT.NONE); label1.setText(Messages.get().Volume); volumes = new Combo(searchComposite, SWT.READ_ONLY); String[] volumeLabels = new String[Index.RF().size()]; for (int i = 0; i < Index.RF().size(); i++) { volumeLabels[i] = Index.Volumes().get(Index.RF().apply(i)).get(); } volumes.setItems(volumeLabels); volumes.setData(JavaConversions.asJavaList(Index.RF())); volumes.select(0); volumes.addSelectionListener(searchListener); Label label2 = new Label(searchComposite, SWT.NONE); label2.setText(Messages.get().Has); } /* DI */@SuppressWarnings("unused") @PostConstruct private void addFocusListener() { searchField .addFocusListener(new SpecialCharacterView.TextFocusListener( searchField)); tagField.addFocusListener(new SpecialCharacterView.TextFocusListener( tagField)); } private enum Navigate { NEXT, PREV } private class NavigationListener implements SelectionListener { private Navigate nav; public NavigationListener(Navigate nav) { this.nav = nav; } @Override public void widgetSelected(SelectionEvent e) { viewer.setSelection(new StructuredSelection()); index = nav == Navigate.PREV ? index - 1 : index + 1; busyCursorWhile(viewer.getControl().getDisplay(), new Runnable() { @Override public void run() { updateSelection(); } }); } @Override public void widgetDefaultSelected(SelectionEvent e) { } } private void addPageInfoBar(Composite parent) { Composite bottomComposite = new Composite(parent, SWT.NONE); bottomComposite.setLayout(new GridLayout(9, false)); Button prev = new Button(bottomComposite, SWT.PUSH | SWT.FLAT); prev.setImage(DrcUiActivator.getDefault().loadImage("icons/prev.gif")); //$NON-NLS-1$ prev.addSelectionListener(new NavigationListener(Navigate.PREV)); Button next = new Button(bottomComposite, SWT.PUSH | SWT.FLAT); next.setImage(DrcUiActivator.getDefault().loadImage("icons/next.gif")); //$NON-NLS-1$ next.addSelectionListener(new NavigationListener(Navigate.NEXT)); currentPageLabel = new Label(bottomComposite, SWT.NONE); Label label = new Label(bottomComposite, SWT.NONE); label.setText(Messages.get().Show + ": "); //$NON-NLS-1$ show = new Combo(bottomComposite, SWT.READ_ONLY); show.setItems(new String[] { Messages.get().All, Messages.get().Open }); show.select(0); show.addSelectionListener(searchListener); insertAddTagButton(bottomComposite); currentPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); addPageCheckedButton(bottomComposite); } private void addPageCheckedButton(Composite parent) { close = new Button(parent, SWT.CHECK); close.setText(Messages.get().ClosePage); close.addListener(SWT.MouseUp, new Listener() { @Override public void handleEvent(org.eclipse.swt.widgets.Event event) { Page page = page(allPages.get(index)); page.status().$plus$eq( new Status(DrcUiActivator.getDefault().currentUser() .id(), System.currentTimeMillis(), close .getSelection())); page.saveToDb(DrcUiActivator.getDefault().currentUser() .collection(), DrcUiActivator.getDefault().db()); viewer.setLabelProvider(new SearchViewLabelProvider()); reload(close.getParent(), page); } }); } private void insertAddTagButton(Composite bottomComposite) { Label label = new Label(bottomComposite, SWT.NONE); label.setText(Messages.get().AddTag); tagField = new Text(bottomComposite, SWT.BORDER); Button addComment = new Button(bottomComposite, SWT.PUSH | SWT.FLAT); addComment.setToolTipText(Messages.get().AddNewTagToCurrentPage); addComment.setImage(DrcUiActivator.getDefault().loadImage( "icons/add.gif")); //$NON-NLS-1$ SelectionListener listener = new SelectionListener() { @Override // on button click public void widgetSelected(SelectionEvent e) { addTag(tagField); } @Override // on enter in text public void widgetDefaultSelected(SelectionEvent e) { addTag(tagField); } private void addTag(final Text text) { String input = text.getText(); if (input != null && input.trim().length() != 0) { Page page = page(allPages.get(index)); page.tags().$plus$eq( new Tag(input, DrcUiActivator.getDefault() .currentUser().id())); page.saveToDb(DrcUiActivator.getDefault().currentUser() .collection(), DrcUiActivator.getDefault().db()); setCurrentPageLabel(page); viewer.setLabelProvider(new SearchViewLabelProvider()); text.setText(""); //$NON-NLS-1$ } } }; addComment.addSelectionListener(listener); tagField.addSelectionListener(listener); // FIXME: Insertion of special characters // tagField.addFocusListener(new // SpecialCharacterView.TextFocusListener(tagField)); } private Page page(String string) { volumes.getDisplay().syncExec(new Runnable() { @Override public void run() { selected = selected(volumes); } }); return Page .fromXml( DrcUiActivator .getDefault() .db() .getXml(DrcUiActivator.getDefault() .currentUser().collection() + "/" + selected, JavaConversions.asScalaBuffer(Arrays.asList(string))).get() //$NON-NLS-1$ .head(), ""); //$NON-NLS-1$ } private void updateSelection() { if (index < allPages.size() && index >= 0) { Page page = page(allPages.get(index)); setCurrentPageLabel(page); StructuredSelection selection = new StructuredSelection( new Page[] { page }); viewer.setSelection(selection); reload(viewer.getTree().getParent(), page); } } private void reload(final Composite parent, final Page page) { if (viewer.getSelection() instanceof List && ((List<?>) viewer.getSelection()).size() == 1) { System.out.println(Messages.get().ReloadingPage + page); parent.getDisplay().asyncExec(new Runnable() { @Override public void run() { Page reloaded = page(page.id()); viewer.setSelection(new StructuredSelection(reloaded)); } }); } } private void setCurrentPageLabel(Page page) { currentPageLabel .setText(String.format( Messages.get().CurrentPageVolume + " %s, " + Messages.get().Page + " %s", volumes.getItem(volumes.getSelectionIndex()), //$NON-NLS-1$ //$NON-NLS-2$ mets.label(page.number()))); close.setSelection(page.done()); } private void select(String pageId) { Page page = page(pageId); Chapter chapter = mets.chapters(page.number(), Count.File()).head(); TreeItem[] items = viewer.getTree().getItems(); for (TreeItem treeItem : items) { if (treeItem.getText(3).contains(chapter.title())) { treeItem.setExpanded(true); } } viewer.refresh(chapter); viewer.setSelection(new StructuredSelection(page)); EditView view = DrcUiActivator.find(EditView.class); view.focusLatestWord(); } private void initSearchField(final Composite parent) { resultCount = new Label(parent, SWT.NONE); updateResultCount(-1); searchField = new Text(parent, SWT.BORDER); searchField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); searchField.addSelectionListener(searchListener); } private void initOptionsCombo(final Composite searchComposite) { Label label = new Label(searchComposite, SWT.NONE); label.setText(Messages.get().In); options.put(Messages.get().Text, SearchOption.all()); options.put(Messages.get().Tags, SearchOption.tags()); options.put(Messages.get().Comments, SearchOption.comments()); searchOptions = new Combo(searchComposite, SWT.READ_ONLY); searchOptions.setItems(options.keySet().toArray( new String[options.keySet().size()])); searchOptions.select(new ArrayList<String>(options.keySet()) .indexOf(Messages.get().Text)); searchOptions.addSelectionListener(searchListener); } private SelectionListener searchListener = new SelectionListener() { @Override public void widgetSelected(final SelectionEvent e) { setInput(); } @Override public void widgetDefaultSelected(final SelectionEvent e) { setInput(); } }; private void updateResultCount(int count) { resultCount .setText(String .format("%s %s " + Messages.get().For, count == -1 ? "[no]" : count, count == 1 ? Messages.get().Hit : Messages.get().Hits)); //$NON-NLS-1$ //$NON-NLS-2$ } private void initTableViewer(final Composite parent) { viewer = new TreeViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER); viewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(final SelectionChangedEvent event) { busyCursorWhile(viewer.getControl().getDisplay(), new Runnable() { @SuppressWarnings("unchecked") @Override public void run() { final IStructuredSelection selection = (IStructuredSelection) event .getSelection(); Object item = selection.getFirstElement(); if (item instanceof Page) { final Page page = (Page) item; toExport = selection.toList(); index = allPages.indexOf(page.id()); setCurrentPageLabel(page); } } }); } }); initTable(); viewer.setContentProvider(new SearchViewContentProvider()); viewer.setLabelProvider(new SearchViewLabelProvider()); DrcUiActivator.getDefault().register(this); getSite().setSelectionProvider(viewer); } private void busyCursorWhile(final Display display, final Runnable runnable) { display.asyncExec(new Runnable() { @Override public void run() { BusyIndicator.showWhile(viewer.getControl().getDisplay(), runnable); } }); } private void initTable() { final int[] columns = new int[] { 60, 50, 60, 400, 200, 150, 100 }; createColumn("", columns[0]); //$NON-NLS-1$ createColumn(Messages.get().Volume, columns[1]); createColumn(Messages.get().Page, columns[2]); createColumn(Messages.get().Text, columns[3]); createColumn(Messages.get().Modified, columns[4]); createColumn(Messages.get().Tags, columns[5]); createColumn(Messages.get().Comments, columns[6]); Tree tree = viewer.getTree(); tree.setHeaderVisible(true); tree.setLinesVisible(true); } private void createColumn(final String name, final int width) { TreeViewerColumn column1 = new TreeViewerColumn(viewer, SWT.NONE); column1.getColumn().setText(name); column1.getColumn().setWidth(width); column1.getColumn().setResizable(true); column1.getColumn().setMoveable(true); } private Map<Chapter, List<Object>> chapters = new TreeMap<Chapter, List<Object>>(); private MetsTransformer mets; private String last = JavaConversions.asJavaList(Index.RF()).get(0); /** * Load SearchView content */ public void setInput() { BusyIndicator.showWhile(viewer.getControl().getDisplay(), new Runnable() { @Override public void run() { load(); } }); } private void pingCollection(final String current, final Page page, final XmlDb db) { /* Ping the collection in the background to avoid delay on first save: */ new Thread(new Runnable() { @Override public void run() { db.putXml(page.toXml(), Index.DefaultCollection() + "/" + current, page.id()); //$NON-NLS-1$ } }).start(); } private void loadData() { content = new SearchViewModelProvider(); } private SearchViewModelProvider content = null; private final class SearchViewModelProvider { Index modelIndex; String modelSelected = null; private SearchViewModelProvider() { viewer.getTree().getDisplay().syncExec(new Runnable() { @Override public void run() { modelSelected = selected(volumes); } }); List<String> ids = JavaConversions.asJavaList(DrcUiActivator .getDefault() .db() .getIds(DrcUiActivator.getDefault().currentUser() .collection() + "/" + modelSelected).get()); //$NON-NLS-1$ // we only load the XML files List<String> pages = new ArrayList<String>(); for (String id : ids) { if (id.endsWith(".xml")) { //$NON-NLS-1$ pages.add(id); } } modelIndex = new Index(JavaConversions.asScalaBuffer(pages) .toList(), DrcUiActivator.getDefault().db(), modelSelected, Index.DefaultCollection()); } Object[] search; public Object[] getPages(final String term) { final String type = searchOptions.getItem(searchOptions .getSelectionIndex()); if (term.trim().equals("")) { //$NON-NLS-1$ search = JavaConversions.asJavaList(modelIndex.pages()) .toArray(new String[modelIndex.pages().size()]); } else { search = null; search = modelIndex.search(term, options.get(type)); System.out.println(String.format( "Searching for '%s' in %s returned %s results", //$NON-NLS-1$ term, type, search.length)); } return search; } } private final class SearchViewContentProvider implements ITreeContentProvider { @Override public Object[] getElements(final Object inputElement) { if (inputElement instanceof Map) { Object[] array = ((Map<?, ?>) inputElement).keySet().toArray( new Chapter[] {}); Arrays.sort(array); return array; } Object[] elements = (Object[]) inputElement; return elements; } @Override public void dispose() { } @Override public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) { } @Override public Object[] getChildren(Object parentElement) { List<Object> ids = chapters.get(parentElement); List<Page> pages = new ArrayList<Page>(); for (Object object : ids) { Page page = object instanceof String ? page((String) object) : (Page) object; if (show.getSelectionIndex() == 0 || !page.done()) { pages.add(page); } } return pages.toArray(new Page[] {}); } @Override public Object getParent(Object element) { return null; } @Override public boolean hasChildren(Object element) { return element instanceof Chapter; } } private boolean isPage(Object element) { return element instanceof Page; } @SuppressWarnings("unchecked") private String selected(Combo volumes) { return "PPN345572629_" //$NON-NLS-1$ + (volumes == null ? JavaConversions.asJavaList(Index.RF()) .get(0) : ((List<String>) volumes.getData()) .get(volumes.getSelectionIndex())); } private Page asPage(Object element) { return (Page) element; } private void load() { String current = selected(volumes); XmlDb db = DrcUiActivator.getDefault().db(); if (content == null || !current.equals(last)) { loadData(); allPages = new ArrayList<String>( JavaConversions.asJavaList(content.modelIndex.pages())); pingCollection(current, page(allPages.get(0)), db); Collections.sort(allPages); } last = current; Object[] pages = content.getPages(searchField.getText().trim() .toLowerCase()); Arrays.sort(pages, comp); chapters = new TreeMap<Chapter, List<Object>>(); boolean meta = true; try { mets = new MetsTransformer(current + ".xml", db); //$NON-NLS-1$ } catch (NullPointerException x) { // No matadata available for selected volume meta = false; } for (Object page : pages) { int fileNumber = page instanceof Page ? ((Page) page).number() : new Page(null, (String) page).number(); List<Chapter> chaptersForPage = meta ? /**/ JavaConversions.asJavaList(mets.chapters(fileNumber, Count.File())) : Arrays.asList(new Chapter(0, 1, Messages.get().NoMeta)); for (Chapter chapter : chaptersForPage) { List<Object> pagesInChapter = chapters.get(chapter); if (pagesInChapter == null) { pagesInChapter = new ArrayList<Object>(); chapters.put(chapter, pagesInChapter); } pagesInChapter.add(page); } } viewer.setInput(chapters); updateResultCount(pages.length); } private final class SearchViewLabelProvider extends LabelProvider implements ITableLabelProvider { @Override public String getColumnText(final Object element, final int columnIndex) { switch (columnIndex) { case 0: return ""; //$NON-NLS-1$ case 1: return isPage(element) ? volumes.getItem(volumes .getSelectionIndex()) : ""; //$NON-NLS-1$ case 2: return isPage(element) && mets != null ? mets.label(asPage( element).number()) + "" : ""; //$NON-NLS-1$ //$NON-NLS-2$ case 3: { if (isPage(element)) { String text = asPage(element).toText("|"); //$NON-NLS-1$ return "Text: " //$NON-NLS-1$ + text.substring(0, Math.min(60, text.length())) + "..."; //$NON-NLS-1$ } return element.toString(); } case 4: return isPage(element) ? lastModificationDate(JavaConversions .asJavaList(asPage(element).words())) : ""; //$NON-NLS-1$ case 5: return isPage(element) ? asPage(element).tags().mkString(", ") : ""; //$NON-NLS-1$ //$NON-NLS-2$ case 6: return isPage(element) ? asPage(element).comments().size() + "" : ""; //$NON-NLS-1$ //$NON-NLS-2$ default: return element.toString(); } } private String lastModificationDate(List<Word> words) { long latest = 0; for (Word word : words) { latest = Math.max(latest, word.history().top().date()); } return new Date(latest).toString(); } @Override public Image getColumnImage(final Object element, final int columnIndex) { if (columnIndex == 0 && element instanceof Page) { Page page = (Page) element; return DrcUiActivator .getDefault() .loadImage( page.done() ? "icons/complete_status.gif" //$NON-NLS-1$ : page.edits() == 0 ? "icons/page.gif" : "icons/edited.gif"); //$NON-NLS-1$ //$NON-NLS-2$ } return null; } } }