/* * Copyright (C) 2011 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.ds; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; 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.DataSource; import com.smartgwt.client.data.DataSourceField; import com.smartgwt.client.data.Record; import com.smartgwt.client.data.RestDataSource; import com.smartgwt.client.data.fields.DataSourceDateTimeField; import com.smartgwt.client.data.fields.DataSourceTextField; import com.smartgwt.client.docs.TreeDataBinding; import com.smartgwt.client.types.CriteriaPolicy; import com.smartgwt.client.types.DSDataFormat; import com.smartgwt.client.types.DSOperationType; import com.smartgwt.client.types.DateDisplayFormat; import com.smartgwt.client.types.FieldType; import com.smartgwt.client.util.BooleanCallback; import cz.cas.lib.proarc.webapp.client.ClientUtils; import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource.DigitalObject; import cz.cas.lib.proarc.webapp.shared.rest.DigitalObjectResourceApi; import java.util.Arrays; import java.util.logging.Logger; /** * Data provider for member relations of digital objects. * <p> * Fetch with {@link #FIELD_ROOT} to get the tree hierarchy of members * including initial PID as root. See {@link TreeDataBinding} for loading tree nodes on demand. * TreeGrid should treat {@link #FIELD_PID} as {@code id} and {@link #FIELD_PARENT} as {@code parentId}. * <p> * Fetch with {@link #FIELD_PARENT} to get direct members. * * @author Jan Pokorsky */ public class RelationDataSource extends RestDataSource { private static final Logger LOG = Logger.getLogger(RelationDataSource.class.getName()); public static final String ID = "RelationDataSource"; private static RelationDataSource INSTANCE; public static final String FIELD_PID = DigitalObjectResourceApi.MEMBERS_ITEM_PID; public static final String FIELD_PARENT = DigitalObjectResourceApi.MEMBERS_ITEM_PARENT; public static final String FIELD_ROOT = DigitalObjectResourceApi.MEMBERS_ROOT_PARAM; public static final String FIELD_MODEL = DigitalObjectResourceApi.MEMBERS_ITEM_MODEL; public static final String FIELD_OWNER = DigitalObjectResourceApi.MEMBERS_ITEM_OWNER; public static final String FIELD_LABEL = DigitalObjectResourceApi.MEMBERS_ITEM_LABEL; public static final String FIELD_STATE = DigitalObjectResourceApi.MEMBERS_ITEM_STATE; public static final String FIELD_CREATED = DigitalObjectResourceApi.MEMBERS_ITEM_CREATED; public static final String FIELD_MODIFIED = DigitalObjectResourceApi.MEMBERS_ITEM_MODIFIED; public static final String FIELD_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_EXPORT; /** * Attribute holding PIDs of the reorder update {@link #reorderChildren operation}. * See also {@link #transformResponse }. */ private static final String ATTR_REORDER = "reorder"; public RelationDataSource() { setID(ID); setDataFormat(DSDataFormat.JSON); setDataURL(RestConfig.URL_DIGOBJECT_CHILDREN); DataSourceField pid = new DataSourceField(FIELD_PID, FieldType.TEXT); pid.setPrimaryKey(true); pid.setRequired(true); DataSourceField parent = new DataSourceField(FIELD_PARENT, FieldType.TEXT); parent.setForeignKey(ID + '.' + FIELD_PID); // canView:false excludes column from grid picker menu parent.setCanView(false); parent.setHidden(true); DataSourceField root = new DataSourceField(FIELD_ROOT, FieldType.TEXT); root.setHidden(true); DataSourceTextField model = new DataSourceTextField(FIELD_MODEL); DataSourceField owner = new DataSourceField(FIELD_OWNER, FieldType.TEXT); DataSourceField label = new DataSourceField(FIELD_LABEL, FieldType.TEXT); DataSourceDateTimeField created = new DataSourceDateTimeField(FIELD_CREATED); created.setDateFormatter(DateDisplayFormat.TOEUROPEANSHORTDATETIME); DataSourceDateTimeField modified = new DataSourceDateTimeField(FIELD_MODIFIED); modified.setDateFormatter(DateDisplayFormat.TOEUROPEANSHORTDATETIME); DataSourceField export = new DataSourceField(FIELD_EXPORT, FieldType.TEXT); setFields(pid, parent, label, model, created, modified, owner, export); setTitleField(FIELD_LABEL); setRequestProperties(RestConfig.createRestRequest(getDataFormat())); setOperationBindings( RestConfig.createAddOperation(), RestConfig.createDeleteOperation(), RestConfig.createUpdatePostOperation() ); setCriteriaPolicy(CriteriaPolicy.DROPONCHANGE); } @Override protected Object transformRequest(DSRequest dsRequest) { if (dsRequest.getOperationType() == DSOperationType.UPDATE && RestConfig.TYPE_APPLICATION_JSON.equals(dsRequest.getContentType())) { return ClientUtils.dump(dsRequest.getData()); } return super.transformRequest(dsRequest); } /** * Checks sent and returned PID lists after reorder request. If they differ * then invalidate cache to refresh widget records. */ @Override protected void transformResponse(DSResponse response, DSRequest request, Object data) { super.transformResponse(response, request, data); // LOG.info("RelationDataSource.transformResponse"); if (RestConfig.isStatusOk(response)) { if (request.getOperationType() == DSOperationType.UPDATE) { String[] oldPids = request.getAttributeAsStringArray(ATTR_REORDER); if (oldPids == null) { return ; } String[] newPids = ClientUtils.toFieldValues(response.getData(), FIELD_PID); if (!Arrays.equals(oldPids, newPids)) { response.setInvalidateCache(Boolean.TRUE); } } } } public static RelationDataSource getInstance() { if (INSTANCE == null) { INSTANCE = (RelationDataSource) DataSource.get(ID); // DataSource.get does not work reliably INSTANCE = INSTANCE != null ? INSTANCE : new RelationDataSource(); } return INSTANCE; } public void addChild(String parentPid, String pid, final BooleanCallback call) { if (pid == null || pid.isEmpty()) { throw new IllegalArgumentException("Missing PID!"); } addChild(parentPid, new String[] {pid}, call); } public void addChild(String parentPid, String[] pid, final BooleanCallback call) { if (pid == null || pid.length < 1) { throw new IllegalArgumentException("Missing PID!"); } if (parentPid == null || parentPid.isEmpty()) { throw new IllegalArgumentException("Missing parent PID!"); } DSRequest dsRequest = new DSRequest(); Record update = new Record(); update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid); update.setAttribute(RelationDataSource.FIELD_PID, pid); addData(update, new DSCallback() { @Override public void execute(DSResponse response, Object rawData, DSRequest request) { if (!RestConfig.isStatusOk(response)) { call.execute(false); return; } call.execute(true); } }, dsRequest); } public void removeChild(String parentPid, String pid, final BooleanCallback call) { if (pid == null || pid.isEmpty()) { throw new IllegalArgumentException("Missing PID!"); } removeChild(parentPid, new String[] {pid}, call); } public void removeChild(String parentPid, String[] pid, final BooleanCallback call) { if (pid == null || pid.length < 1) { throw new IllegalArgumentException("Missing PID!"); } if (parentPid == null || parentPid.isEmpty()) { throw new IllegalArgumentException("Missing parent PID!"); } Record update = new Record(); update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid); update.setAttribute(RelationDataSource.FIELD_PID, pid); DSRequest dsRequest = new DSRequest(); dsRequest.setData(update); // prevents removeData to drop other than primary key attributes removeData(update, new DSCallback() { @Override public void execute(DSResponse response, Object rawData, DSRequest request) { if (!RestConfig.isStatusOk(response)) { call.execute(false); return; } call.execute(true); } }, dsRequest); } public void moveChild(final String pid, final String oldParentPid, final String parentPid, final BooleanCallback call) { moveChild(new String[] {pid}, oldParentPid, parentPid, call); } public void moveChild(final String[] pid, final String parentPidFrom, final String parentPidTo, final BooleanCallback call) { moveChild(pid, parentPidFrom, parentPidTo, null, call); } public void moveChild(final String[] pid, final String parentPidFrom, final String parentPidTo, final Integer batchId, final BooleanCallback call) { if (pid == null || pid.length < 1) { throw new IllegalArgumentException("Missing PID!"); } if (parentPidFrom == null || parentPidFrom.isEmpty()) { throw new IllegalArgumentException("Missing source parent PID!"); } if (parentPidTo == null || parentPidTo.isEmpty()) { throw new IllegalArgumentException("Missing target parent PID!"); } Record update = new Record(); update.setAttribute(DigitalObjectResourceApi.MEMBERS_MOVE_SRCPID, parentPidFrom); update.setAttribute(DigitalObjectResourceApi.MEMBERS_MOVE_DSTPID, parentPidTo); update.setAttribute(RelationDataSource.FIELD_PID, pid); if (batchId != null) { update.setAttribute(DigitalObjectResourceApi.MEMBERS_ITEM_BATCHID, batchId); } final RelationDataSource ds = RelationDataSource.getInstance(); DSRequest request = new DSRequest(); request.setActionURL(RestConfig.URL_DIGOBJECT_CHILDREN_MOVE); ds.updateData(update, new DSCallback() { @Override public void execute(DSResponse response, Object data, DSRequest request) { if (!RestConfig.isStatusOk(response)) { call.execute(false); return; } call.execute(true); } }, request); } /** * Saves new children sequence for a given parent object. * * @param parent parent object * @param childPids children sequence * @param call callback */ public void reorderChildren(DigitalObject parent, String[] childPids, final BooleanCallback call) { if (childPids == null || childPids.length < 2) { throw new IllegalArgumentException("Unexpected children: " + Arrays.toString(childPids)); } if (parent == null) { throw new NullPointerException("parent"); } DSRequest dsRequest = new DSRequest(); dsRequest.setAttribute(ATTR_REORDER, childPids); Record update = new Record(); update.setAttribute(RelationDataSource.FIELD_PID, childPids); String parentPid = parent.getPid(); String batchId = parent.getBatchId(); if (batchId != null) { update.setAttribute(DigitalObjectResourceApi.MEMBERS_ITEM_BATCHID, batchId); } else { update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid); } updateData(update, new DSCallback() { @Override public void execute(DSResponse response, Object rawData, DSRequest request) { if (!RestConfig.isStatusOk(response)) { call.execute(false); return; } call.execute(true); } }, dsRequest); } /** * Compares arrays of PIDS as array of records. */ public static boolean equals(Record[] rs1, Record[] rs2) { if (rs1 == null && rs1 == rs2) { return true; } if (rs1 == null || rs2 == null) { return false; } if (rs1.length != rs2.length) { return false; } for (int i = 0; i < rs1.length; i++) { String pid1 = rs1[i].getAttribute(RelationDataSource.FIELD_PID); String pid2 = rs2[i].getAttribute(RelationDataSource.FIELD_PID); if (!pid1.equals(pid2)) { return false; } } return true; } /** * Invoked by other digital object editors that can change object label. * @param pid PID of modified object */ public void fireRelationChange(String pid) { fireEvent(new RelationChangeEvent(pid)); } /** * Fetches relations of parent object and update caches to notify widgets. * @param parentPid PID of parent object * @param callback the callback to run on finish */ public final void updateCaches(String parentPid, final BooleanCallback callback) { Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid); criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid); fetchData(criteria, new DSCallback() { @Override public void execute(DSResponse response, Object rawData, DSRequest request) { request.setOperationType(DSOperationType.UPDATE); updateCaches(response, request); callback.execute(Boolean.TRUE); } }); } public final HandlerRegistration addRelationChangeHandler(RelationChangeHandler handler) { return doAddHandler(handler, RelationChangeEvent.TYPE); } /** * Notifies changes of relation labels. */ public static interface RelationChangeHandler extends EventHandler { void onRelationChange(RelationChangeEvent event); } public static final class RelationChangeEvent extends GwtEvent<RelationChangeHandler> { public static final Type<RelationChangeHandler> TYPE = new Type<RelationChangeHandler>(); private final String pid; public RelationChangeEvent() { this(null); } public RelationChangeEvent(String pid) { this.pid = pid; } public String getPid() { return pid; } @Override public Type<RelationChangeHandler> getAssociatedType() { return TYPE; } @Override protected void dispatch(RelationChangeHandler handler) { handler.onRelationChange(this); } } }