blob: 5c6fcce2976102dabc9dabf526f2e055a4594637 [file] [log] [blame]
// Copyright 2018 The Feed Authors.
//
// 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 com.google.android.libraries.feed.feedprotocoladapter;
import com.google.android.libraries.feed.api.internal.protocoladapter.ProtocolAdapter;
import com.google.android.libraries.feed.common.Result;
import com.google.android.libraries.feed.common.Validators;
import com.google.android.libraries.feed.common.logging.Dumpable;
import com.google.android.libraries.feed.common.logging.Dumper;
import com.google.android.libraries.feed.common.logging.Logger;
import com.google.android.libraries.feed.common.time.TimingUtils;
import com.google.android.libraries.feed.common.time.TimingUtils.ElapsedTimeTracker;
import com.google.android.libraries.feed.feedprotocoladapter.internal.transformers.ContentDataOperationTransformer;
import com.google.android.libraries.feed.feedprotocoladapter.internal.transformers.DataOperationTransformer;
import com.google.android.libraries.feed.feedprotocoladapter.internal.transformers.FeatureDataOperationTransformer;
import com.google.protobuf.ByteString;
import com.google.search.now.feed.client.StreamDataProto.StreamDataOperation;
import com.google.search.now.feed.client.StreamDataProto.StreamFeature;
import com.google.search.now.feed.client.StreamDataProto.StreamPayload;
import com.google.search.now.feed.client.StreamDataProto.StreamSharedState;
import com.google.search.now.feed.client.StreamDataProto.StreamStructure;
import com.google.search.now.feed.client.StreamDataProto.StreamStructure.Operation;
import com.google.search.now.feed.client.StreamDataProto.StreamToken;
import com.google.search.now.wire.feed.ContentIdProto.ContentId;
import com.google.search.now.wire.feed.ContentIdProto.ContentId.Builder;
import com.google.search.now.wire.feed.DataOperationProto.DataOperation;
import com.google.search.now.wire.feed.FeatureProto.Feature;
import com.google.search.now.wire.feed.FeatureProto.Feature.RenderableUnit;
import com.google.search.now.wire.feed.FeedResponseProto.FeedResponse;
import com.google.search.now.wire.feed.FeedResponseProto.FeedResponseMetadata;
import com.google.search.now.wire.feed.OpaqueActionDataProto.OpaqueActionData;
import com.google.search.now.wire.feed.PietSharedStateItemProto.PietSharedStateItem;
import com.google.search.now.wire.feed.ResponseProto.Response;
import com.google.search.now.wire.feed.TokenProto.Token;
import java.util.ArrayList;
import java.util.List;
/** A ProtocolAdapter which converts from the wire protocol to the internal protocol. */
public final class FeedProtocolAdapter implements ProtocolAdapter, Dumpable {
private static final String TAG = "FeedProtocolAdapter";
static final String CONTENT_ID_DELIMITER = "::";
private final List<DataOperationTransformer> dataOperationTransformers;
private final TimingUtils timingUtils;
// Operation counts for #dump(Dumpable)
private int responseHandlingCount = 0;
private int convertContentIdCount = 0;
public FeedProtocolAdapter(TimingUtils timingUtils) {
this.timingUtils = timingUtils;
dataOperationTransformers = new ArrayList<>(2);
dataOperationTransformers.add(new FeatureDataOperationTransformer());
dataOperationTransformers.add(new ContentDataOperationTransformer());
}
@Override
public String getStreamContentId(ContentId contentId) {
convertContentIdCount++;
return createContentId(contentId);
}
@Override
public Result<ContentId> getWireContentId(String contentId) {
String[] splitContentId = contentId.split(CONTENT_ID_DELIMITER, -1);
// Can't create if all 3 parts aren't present (at the very least empty)
if (splitContentId.length != 3) {
Logger.e(TAG, "Error parsing string content ID - splitting did not result in 3 parts");
return Result.failure();
}
String table = splitContentId[0];
String contentDomain = splitContentId[1];
long id;
try {
id = Long.parseLong(splitContentId[2]);
} catch (NumberFormatException e) {
Logger.e(TAG, e, "Error converting content ID to wire format");
return Result.failure();
}
Builder builder = ContentId.newBuilder().setId(id);
if (!table.isEmpty()) {
builder.setTable(table);
}
if (!contentDomain.isEmpty()) {
builder.setContentDomain(contentDomain);
}
return Result.success(builder.build());
}
@Override
public Result<List<StreamDataOperation>> createModel(Response response) {
responseHandlingCount++;
FeedResponse feedResponse = response.getExtension(FeedResponse.feedResponse);
Logger.i(TAG, "createModel, operations %s", feedResponse.getDataOperationCount());
return createOperations(
feedResponse.getDataOperationList(), feedResponse.getFeedResponseMetadata());
}
@Override
public Result<List<StreamDataOperation>> createOperations(List<DataOperation> dataOperations) {
return createOperations(dataOperations, FeedResponseMetadata.getDefaultInstance());
}
private Result<List<StreamDataOperation>> createOperations(
List<DataOperation> dataOperations, FeedResponseMetadata responseMetadata) {
ElapsedTimeTracker totalTimeTracker = timingUtils.getElapsedTimeTracker(TAG);
List<StreamDataOperation> streamDataOperations = new ArrayList<>();
for (DataOperation operation : dataOperations) {
// The operations defined in stream_data.proto and data_operation.proto have the same
// integer value
Operation streamOperation = Operation.forNumber(operation.getOperation().getNumber());
String contentId;
if (streamOperation == Operation.CLEAR_ALL) {
streamDataOperations.add(createDataOperation(Operation.CLEAR_ALL, null, null).build());
continue;
} else if (streamOperation == Operation.REMOVE) {
if (operation.getMetadata().hasContentId()) {
contentId = createContentId(operation.getMetadata().getContentId());
String parentId = null;
if (operation.getFeature().hasParentId()) {
parentId = createContentId(operation.getFeature().getParentId());
}
streamDataOperations.add(
createDataOperation(Operation.REMOVE, contentId, parentId).build());
} else {
Logger.w(TAG, "REMOVE defined without a ContentId identifying the item to remove");
}
continue;
} else if (operation.getMetadata().hasContentId()) {
contentId = createContentId(operation.getMetadata().getContentId());
} else {
// This is an error state, every card should have a content id
Logger.e(TAG, "ContentId not defined for DataOperation");
continue;
}
if (operation.hasFeature()) {
handleFeatureOperation(operation, responseMetadata, contentId, streamDataOperations);
} else if (operation.hasPietSharedState()) {
PietSharedStateItem item =
PietSharedStateItem.newBuilder()
.setPietSharedState(operation.getPietSharedState())
.build();
StreamSharedState state =
StreamSharedState.newBuilder()
.setPietSharedStateItem(item)
.setContentId(contentId)
.build();
streamDataOperations.add(
createSharedStateDataOperation(streamOperation, contentId, state).build());
}
if (operation.getMetadata().getSemanticProperties().hasSemanticPropertiesData()) {
streamDataOperations.add(
createSemanticDataOperation(
contentId,
operation.getMetadata().getSemanticProperties().getSemanticPropertiesData())
.build());
}
if (operation.getMetadata().getActionProperties().hasActionData()) {
streamDataOperations.add(
createActionDataOperation(
contentId, operation.getMetadata().getActionProperties().getActionData())
.build());
}
}
totalTimeTracker.stop("task", "convertWireProtocol", "mutations", dataOperations.size());
return Result.success(streamDataOperations);
}
private void handleFeatureOperation(
DataOperation operation,
FeedResponseMetadata feedResponseMetadata,
String contentId,
List<StreamDataOperation> streamDataOperations) {
Operation streamOperation = Operation.forNumber(operation.getOperation().getNumber());
String parentId = null;
if (operation.getFeature().hasParentId()) {
parentId = createContentId(operation.getFeature().getParentId());
}
if (operation.getFeature().getRenderableUnit() == RenderableUnit.TOKEN) {
Feature feature = operation.getFeature();
if (feature.hasExtension(Token.tokenExtension)) {
// Create a StreamToken operation
Token token = feature.getExtension(Token.tokenExtension);
StreamToken streamToken =
Validators.checkNotNull(
createStreamToken(contentId, parentId, token.getNextPageToken()));
streamDataOperations.add(
createTokenDataOperation(contentId, streamToken.getParentId(), streamToken).build());
} else {
Logger.e(TAG, "Extension not found for TOKEN");
}
} else {
StreamFeature.Builder streamFeatureBuilder = createStreamFeature(contentId, parentId);
StreamDataOperation.Builder streamDataOperation =
StreamDataOperation.newBuilder()
.setStreamPayload(StreamPayload.newBuilder().setStreamFeature(streamFeatureBuilder));
streamDataOperation =
transformOperation(operation, streamDataOperation, feedResponseMetadata);
streamDataOperation =
createDataOperation(streamDataOperation, streamOperation, contentId, parentId);
streamDataOperations.add(streamDataOperation.build());
}
}
private StreamDataOperation.Builder transformOperation(
DataOperation operation,
StreamDataOperation.Builder streamDataOperation,
FeedResponseMetadata feedResponseMetadata) {
for (DataOperationTransformer transformer : dataOperationTransformers) {
streamDataOperation =
transformer.transform(operation, streamDataOperation, feedResponseMetadata);
}
return streamDataOperation;
}
private StreamFeature.Builder createStreamFeature(String contentId, /*@Nullable*/ String parentId) {
StreamFeature.Builder builder = StreamFeature.newBuilder();
builder.setContentId(contentId);
if (parentId != null) {
builder.setParentId(parentId);
}
return builder;
}
/*@Nullable*/
private StreamToken createStreamToken(
String tokenId, /*@Nullable*/ String parentId, ByteString continuationToken) {
if (continuationToken.isEmpty()) {
return null;
}
StreamToken.Builder tokenBuilder = StreamToken.newBuilder();
if (parentId != null) {
tokenBuilder.setParentId(parentId);
}
tokenBuilder.setContentId(tokenId);
tokenBuilder.setNextPageToken(continuationToken);
return tokenBuilder.build();
}
private StreamDataOperation.Builder createSharedStateDataOperation(
Operation operation, /*@Nullable*/ String contentId, StreamSharedState sharedState) {
StreamDataOperation.Builder dataOperation = createDataOperation(operation, contentId, null);
dataOperation.setStreamPayload(StreamPayload.newBuilder().setStreamSharedState(sharedState));
return dataOperation;
}
private StreamDataOperation.Builder createTokenDataOperation(
/*@Nullable*/ String contentId, /*@Nullable*/ String parentId, StreamToken streamToken) {
StreamDataOperation.Builder dataOperation =
createDataOperation(Operation.UPDATE_OR_APPEND, contentId, parentId);
dataOperation.setStreamPayload(StreamPayload.newBuilder().setStreamToken(streamToken));
return dataOperation;
}
private StreamDataOperation.Builder createDataOperation(
Operation operation, /*@Nullable*/ String contentId, /*@Nullable*/ String parentId) {
return createDataOperation(StreamDataOperation.newBuilder(), operation, contentId, parentId);
}
private StreamDataOperation.Builder createDataOperation(
StreamDataOperation.Builder streamDataOperation,
Operation operation,
/*@Nullable*/ String contentId,
/*@Nullable*/ String parentId) {
StreamStructure.Builder streamStructure = StreamStructure.newBuilder();
streamStructure.setOperation(operation);
if (contentId != null) {
streamStructure.setContentId(contentId);
}
if (parentId != null) {
streamStructure.setParentContentId(parentId);
}
streamDataOperation.setStreamStructure(streamStructure);
return streamDataOperation;
}
private StreamDataOperation.Builder createSemanticDataOperation(
String contentId, ByteString semanticData) {
return StreamDataOperation.newBuilder()
.setStreamPayload(StreamPayload.newBuilder().setSemanticData(semanticData))
.setStreamStructure(
StreamStructure.newBuilder()
.setContentId(contentId)
.setOperation(Operation.UPDATE_OR_APPEND));
}
private StreamDataOperation.Builder createActionDataOperation(
String contentId, OpaqueActionData actionData) {
return StreamDataOperation.newBuilder()
.setStreamPayload(StreamPayload.newBuilder().setActionData(actionData))
.setStreamStructure(
StreamStructure.newBuilder()
.setContentId(contentId)
.setOperation(Operation.UPDATE_OR_APPEND));
}
private String createContentId(ContentId contentId) {
// Using String concat for performance reasons. This is called a lot for large feed responses.
return contentId.getTable()
+ CONTENT_ID_DELIMITER
+ contentId.getContentDomain()
+ CONTENT_ID_DELIMITER
+ contentId.getId();
}
@Override
public void dump(Dumper dumper) {
dumper.title(TAG);
dumper.forKey("responseHandlingCount").value(responseHandlingCount);
dumper.forKey("convertContentIdCount").value(convertContentIdCount).compactPrevious();
}
}