/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.QueryResponse;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.query.EntityResultSegment;
import org.apache.cayenne.query.PrefetchTreeNode;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.util.GenericResponse;
import org.apache.cayenne.util.IncrementalListResponse;
import org.apache.cayenne.util.ListResponse;
import org.apache.cayenne.util.ObjectDetachOperation;
import java.util.ArrayList;
import java.util.List;
/**
* A query handler used by ClientServerChannel.
*
* @since 1.2
*/
class ClientServerChannelQueryAction {
static final boolean DONE = true;
private final ClientServerChannel channel;
private Query serverQuery;
private QueryResponse response;
private final QueryMetadata serverMetadata;
private final EntityResolver serverResolver;
ClientServerChannelQueryAction(ClientServerChannel channel, Query query) {
this.channel = channel;
this.serverResolver = channel.getEntityResolver();
this.serverQuery = query;
this.serverMetadata = serverQuery.getMetaData(serverResolver);
}
QueryResponse execute() {
if (interceptSinglePageQuery() != DONE) {
runQuery();
}
if (interceptIncrementalListConversion() != DONE) {
interceptObjectConversion();
}
return response;
}
private boolean interceptSinglePageQuery() {
// retrieve range from the previously cached list
if (serverMetadata.getFetchOffset() >= 0
&& serverMetadata.getFetchLimit() > 0
&& serverMetadata.getCacheKey() != null) {
List cachedList = channel.getQueryCache().get(serverMetadata);
if (cachedList == null) {
// attempt to refetch... respawn the action...
Query originatingQuery = serverMetadata.getOriginatingQuery();
if (originatingQuery != null) {
ClientServerChannelQueryAction subaction = new ClientServerChannelQueryAction(
channel,
originatingQuery);
subaction.execute();
cachedList = channel.getQueryCache().get(serverMetadata);
if (cachedList == null) {
throw new CayenneRuntimeException("No cached list for %s", serverMetadata.getCacheKey());
}
} else {
return !DONE;
}
}
int startIndex = serverMetadata.getFetchOffset();
int endIndex = startIndex + serverMetadata.getFetchLimit();
// send back just one page... query sender will figure out where it fits in
// the incremental list
this.response = new ListResponse(new ArrayList<>(cachedList.subList(
startIndex,
endIndex)));
return DONE;
}
return !DONE;
}
private void runQuery() {
this.response = channel.getParentChannel().onQuery(null, serverQuery);
}
private boolean interceptIncrementalListConversion() {
int pageSize = serverMetadata.getPageSize();
if (pageSize > 0 && serverMetadata.getCacheKey() != null) {
List list = response.firstList();
if (list.size() > pageSize && list instanceof IncrementalFaultList) {
// cache
channel.getQueryCache().put(serverMetadata, list);
// extract and convert first page
// TODO: andrus, 2008/03/05 - we no longer resolve the first page
// automatically on the server... probably should not do it for the client
// either... One rare case when this is completely undesirable is
// subaction execution from 'interceptSinglePageQuery', as it doesn't even
// care about the first page...
List sublist = list.subList(0, pageSize);
List firstPage = (serverMetadata.isFetchingDataRows()) ? new ArrayList(sublist) : toClientObjects(sublist);
this.response = new IncrementalListResponse(firstPage, list.size());
return DONE;
}
}
return !DONE;
}
private void interceptObjectConversion() {
if (!serverMetadata.isFetchingDataRows()) {
GenericResponse clientResponse = new GenericResponse();
for (response.reset(); response.next();) {
if (response.isList()) {
List serverObjects = response.currentList();
clientResponse.addResultList(toClientObjects(serverObjects));
}
else {
clientResponse.addBatchUpdateCount(response.currentUpdateCount());
}
}
this.response = clientResponse;
}
}
private List toClientObjects(List serverObjects) {
if (!serverObjects.isEmpty()) {
List<Object> rsMapping = serverMetadata.getResultSetMapping();
if (rsMapping == null) {
return singleObjectConversion(serverObjects);
}
else {
if (rsMapping.size() == 1) {
if (rsMapping.get(0) instanceof EntityResultSegment) {
return singleObjectConversion(serverObjects);
}
else {
// we can return a single scalar result unchanged (hmm... a scalar
// Object[] can also be returned unchanged)...
return serverObjects;
}
}
else {
return processMixedResult(serverObjects, rsMapping);
}
}
}
return new ArrayList<>(3);
}
private List<Object[]> processMixedResult(
List<Object[]> serverObjects,
List<Object> rsMapping) {
// must clone the list to ensure we do not mess up the server list that can be
// used elsewhere (e.g. it can be cached).
List<Object[]> clientObjects = new ArrayList<>(serverObjects.size());
ObjectDetachOperation op = new ObjectDetachOperation(serverResolver
.getClientEntityResolver());
int width = rsMapping.size();
for (Object[] serverObject : serverObjects) {
Object[] clientObject = new Object[width];
for (int i = 0; i < width; i++) {
if (rsMapping.get(i) instanceof EntityResultSegment) {
clientObject[i] = convertSingleObject(serverMetadata
.getPrefetchTree(), op, serverObject[i]);
}
else {
clientObject[i] = serverObject[i];
}
}
clientObjects.add(clientObject);
}
return clientObjects;
}
private List<?> singleObjectConversion(List<?> serverObjects) {
// must clone the list to ensure we do not mess up the server list that can be
// used elsewhere (e.g. it can be cached).
List<Object> clientObjects = new ArrayList<>(serverObjects.size());
ObjectDetachOperation op = new ObjectDetachOperation(serverResolver
.getClientEntityResolver());
PrefetchTreeNode prefetchTree = serverMetadata.getPrefetchTree();
for (Object serverObject : serverObjects) {
clientObjects.add(convertSingleObject(prefetchTree, op, serverObject));
}
return clientObjects;
}
private Object convertSingleObject(
PrefetchTreeNode prefetchTree,
ObjectDetachOperation op,
Object serverObject) {
Persistent object = (Persistent) serverObject;
ObjectId id = object.getObjectId();
// sanity check
if (id == null) {
throw new CayenneRuntimeException("Server returned an object without an id: %s", object);
}
// have to resolve descriptor here for every object, as
// often a query will not have any info indicating the
// entity type
ClassDescriptor serverDescriptor = serverResolver.getClassDescriptor(id
.getEntityName());
return op.detach(object, serverDescriptor, prefetchTree);
}
}