/*
* Copyright (C) 2013 Jan Pokorsky
*
* This program 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.
*
* This program 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 cz.cas.lib.proarc.webapp.client.action;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceController;
import com.smartgwt.client.data.Criteria;
import com.smartgwt.client.data.DSCallback;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.data.RecordList;
import com.smartgwt.client.data.ResultSet;
import com.smartgwt.client.types.PromptStyle;
import com.smartgwt.client.util.Page;
import com.smartgwt.client.util.SC;
import cz.cas.lib.proarc.common.object.model.DatastreamEditorType;
import cz.cas.lib.proarc.webapp.client.ClientMessages;
import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource.DigitalObject;
import cz.cas.lib.proarc.webapp.client.ds.RelationDataSource;
import cz.cas.lib.proarc.webapp.client.ds.RestConfig;
import cz.cas.lib.proarc.webapp.client.ds.SearchDataSource;
import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditing.DigitalObjectEditorPlace;
/**
* Opens parent, child or sibling of selected digital object in the current editor.
* In order to navigate to given child object the action source has to implement
* {@link ChildSelector}. Otherwise the first child is used.
*
* @author Jan Pokorsky
*/
public final class DigitalObjectNavigateAction extends AbstractAction {
public enum Navigation {CHILD, NEXT, PARENT, PREV}
private final PlaceController places;
private final ClientMessages i18n;
private final Navigation navigation;
private static RecordList siblings;
public static DigitalObjectNavigateAction parent(ClientMessages i18n, PlaceController places) {
return new DigitalObjectNavigateAction(i18n,
i18n.DigitalObjectNavigateAction_OpenParent_Title(),
Page.getAppDir() + "images/16/next_up.png",
i18n.DigitalObjectNavigateAction_OpenParent_Hint(),
Navigation.PARENT,
places);
}
public static DigitalObjectNavigateAction child(ClientMessages i18n, PlaceController places) {
return new DigitalObjectNavigateAction(i18n,
i18n.DigitalObjectNavigateAction_OpenChild_Title(),
Page.getAppDir() + "images/16/next_down.png",
i18n.DigitalObjectNavigateAction_OpenChild_Hint(),
Navigation.CHILD,
places);
}
public static DigitalObjectNavigateAction next(ClientMessages i18n, PlaceController places) {
return new DigitalObjectNavigateAction(i18n,
i18n.DigitalObjectNavigateAction_OpenNext_Title(),
"[SKIN]/actions/next.png",
i18n.DigitalObjectNavigateAction_OpenNext_Hint(),
Navigation.NEXT,
places);
}
public static DigitalObjectNavigateAction previous(ClientMessages i18n, PlaceController places) {
return new DigitalObjectNavigateAction(i18n,
i18n.DigitalObjectNavigateAction_OpenPrevious_Title(),
"[SKIN]/actions/prev.png",
i18n.DigitalObjectNavigateAction_OpenPrevious_Hint(),
Navigation.PREV,
places);
}
public DigitalObjectNavigateAction(
ClientMessages i18n, String title, String icon, String tooltip, Navigation navigation,
PlaceController places) {
super(title, icon, tooltip);
this.navigation = navigation;
this.places = places;
this.i18n = i18n;
}
@Override
public void performAction(ActionEvent event) {
Record[] selectedRecords = Actions.getSelection(event);
if (accept(selectedRecords)) {
DigitalObject dobj = DigitalObject.create(selectedRecords[0]);
String pid = dobj.getPid();
switch (navigation) {
case PARENT:
openParent(pid);
break;
case NEXT:
case PREV:
openSibling(pid, false);
break;
case CHILD:
openChild(pid, getChildSelection(event));
break;
}
}
}
@Override
public boolean accept(ActionEvent event) {
Record[] selectedRecords = Actions.getSelection(event);
return accept(selectedRecords);
}
private boolean accept(Record[] selectedRecords) {
if (selectedRecords != null && selectedRecords.length == 1) {
return isDigitalObject(selectedRecords[0]);
}
return false;
}
private static boolean isDigitalObject(Record r) {
DigitalObject dobj = DigitalObject.createOrNull(r);
return dobj != null;
}
private void openParent(final String childPid) {
if (childPid != null) {
SearchDataSource.getInstance().findParent(childPid, null, new Callback<ResultSet, Void>() {
@Override
public void onFailure(Void reason) {
}
@Override
public void onSuccess(ResultSet result) {
if (result.isEmpty()) {
SC.warn(i18n.DigitalObjectNavigateAction_NoParent_Msg());
} else {
Record parent = result.first();
DigitalObject parentObj = DigitalObject.createOrNull(parent);
if (parentObj != null) {
siblings = null;
DigitalObjectEditorPlace place = new DigitalObjectEditorPlace(
getLastEditorId(), parentObj);
place.setSelectChildPid(childPid);
places.goTo(place);
}
}
}
});
}
}
private void open(DigitalObject dobj) {
if (dobj != null) {
DigitalObjectEditorPlace place = new DigitalObjectEditorPlace(
getLastEditorId(), dobj);
places.goTo(place);
}
}
/**
* Opens a child object in the editor. If child is {@code null} it fetches
* children and opens the first one.
*/
private void openChild(final String parentPid, DigitalObject child) {
if (child != null) {
siblings = null;
open(child);
return ;
}
Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid);
criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid);
RelationDataSource.getInstance().fetchData(criteria, new DSCallback() {
@Override
public void execute(DSResponse dsResponse, Object data, DSRequest dsRequest) {
if (RestConfig.isStatusOk(dsResponse)) {
RecordList result = dsResponse.getDataAsRecordList();
DigitalObject dobj = null;
if (!result.isEmpty()) {
dobj = DigitalObject.createOrNull(result.get(0));
}
if (dobj != null) {
siblings = result;
open(dobj);
} else {
// No child
SC.warn(i18n.DigitalObjectNavigateAction_NoChild_Msg());
}
}
}
}, createRequestWithPrompt());
}
private void openSibling(final String pid, boolean cached) {
if (pid != null) {
RecordList rs = getSiblings();
int pidIndex = rs.findIndex(RelationDataSource.FIELD_PID, pid);
if (pidIndex == -1) {
// fetch
if (cached) {
// not found PID
SC.warn("Not found " + pid);
} else {
fetchSiblings(pid);
}
} else if (navigation == Navigation.PREV && pidIndex == 0) {
SC.warn(i18n.DigitalObjectNavigateAction_NoPrevSibling_Msg());
} else if (navigation == Navigation.NEXT && pidIndex + 1 >= rs.getLength()) {
SC.warn(i18n.DigitalObjectNavigateAction_NoNextSibling_Msg());
} else {
// open new
int inc = navigation == Navigation.PREV ? -1 : 1;
DigitalObject newObj = DigitalObject.create(rs.get(pidIndex + inc));
if (newObj != null) {
open(newObj);
}
}
}
}
private RecordList getSiblings() {
if (siblings == null) {
// listen to RelationDataSource updates to invalidate cache?
siblings = new RecordList();
}
return siblings;
}
private void fetchSiblings(final String pid) {
SearchDataSource.getInstance().findParent(pid, null, new Callback<ResultSet, Void>() {
@Override
public void onFailure(Void reason) {
}
@Override
public void onSuccess(ResultSet result) {
if (result.isEmpty()) {
SC.warn(i18n.DigitalObjectNavigateAction_NoParent_Msg());
} else {
Record parent = result.first();
DigitalObject parentObj = DigitalObject.createOrNull(parent);
if (parentObj != null) {
scheduleFetchSiblings(parentObj.getPid(), pid);
}
}
}
});
}
/**
* Postpones {@link #fetchSiblings(java.lang.String, java.lang.String) fetch}
* to force RPCManager to notify user with the request prompt. The invocation
* from ResultSet's DataArrivedHandler ignores prompt settings and ResultSet
* does not provide possibility to declare the prompt.
*/
private void scheduleFetchSiblings(final String parentPid, final String pid) {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
fetchSiblings(parentPid, pid);
}
});
}
private void fetchSiblings(final String parentPid, final String pid) {
Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid);
criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid);
RelationDataSource.getInstance().fetchData(criteria, new DSCallback() {
@Override
public void execute(DSResponse dsResponse, Object data, DSRequest dsRequest) {
if (RestConfig.isStatusOk(dsResponse)) {
siblings = dsResponse.getDataAsRecordList();
openSibling(pid, true);
}
}
}, createRequestWithPrompt());
}
private DatastreamEditorType getLastEditorId() {
DatastreamEditorType editorId = null;
Place where = places.getWhere();
if (where instanceof DigitalObjectEditorPlace) {
DigitalObjectEditorPlace editorPlace = (DigitalObjectEditorPlace) where;
editorId = editorPlace.getEditorId();
}
return editorId == null ? DatastreamEditorType.CHILDREN : editorId;
}
private static DigitalObject getChildSelection(ActionEvent event) {
Object source = event.getSource();
if (source instanceof ChildSelector) {
ChildSelector selectable = (ChildSelector) source;
Record[] children = selectable.getChildSelection();
if (children != null && children.length > 0) {
return DigitalObject.createOrNull(children[0]);
}
}
return null;
}
private static DSRequest createRequestWithPrompt() {
DSRequest dsRequest = new DSRequest();
dsRequest.setPromptStyle(PromptStyle.CURSOR);
dsRequest.setShowPrompt(true);
return dsRequest;
}
/**
* The action source should implement this to supply children in case of
* the child navigation.
*/
public interface ChildSelector {
/**
* Gets an array of child records.
*/
Record[] getChildSelection();
}
}