com.google.api.client.googleapis.xml.atom.GoogleAtom Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2010 Google Inc.
*
* 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.api.client.googleapis.xml.atom;
import com.google.api.client.util.ArrayMap;
import com.google.api.client.util.Beta;
import com.google.api.client.util.ClassInfo;
import com.google.api.client.util.Data;
import com.google.api.client.util.FieldInfo;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Types;
import java.util.Collection;
import java.util.Map;
import java.util.TreeSet;
/**
* {@link Beta}
* Utilities for working with the Atom XML of Google Data APIs.
*
* @since 1.0
* @author Yaniv Inbar
*/
@Beta
public class GoogleAtom {
/**
* GData namespace.
*
* @since 1.0
*/
public static final String GD_NAMESPACE = "http://schemas.google.com/g/2005";
/**
* Content type used on an error formatted in XML.
*
* @since 1.5
*/
public static final String ERROR_CONTENT_TYPE = "application/vnd.google.gdata.error+xml";
// TODO(yanivi): require XmlNamespaceDictory and include xmlns declarations since there is no
// guarantee that there is a match between Google's mapping and the one used by client
/**
* Returns the fields mask to use for the given data class of key/value pairs. It cannot be a
* {@link Map}, {@link GenericData} or a {@link Collection}.
*
* @param dataClass data class of key/value pairs
*/
public static String getFieldsFor(Class> dataClass) {
StringBuilder fieldsBuf = new StringBuilder();
appendFieldsFor(fieldsBuf, dataClass, new int[1]);
return fieldsBuf.toString();
}
/**
* Returns the fields mask to use for the given data class of key/value pairs for the feed class
* and for the entry class. This should only be used if the feed class does not contain the entry
* class as a field. The data classes cannot be a {@link Map}, {@link GenericData} or a {@link
* Collection}.
*
* @param feedClass feed data class
* @param entryClass entry data class
*/
public static String getFeedFields(Class> feedClass, Class> entryClass) {
StringBuilder fieldsBuf = new StringBuilder();
appendFeedFields(fieldsBuf, feedClass, entryClass);
return fieldsBuf.toString();
}
private static void appendFieldsFor(
StringBuilder fieldsBuf, Class> dataClass, int[] numFields) {
if (Map.class.isAssignableFrom(dataClass) || Collection.class.isAssignableFrom(dataClass)) {
throw new IllegalArgumentException(
"cannot specify field mask for a Map or Collection class: " + dataClass);
}
ClassInfo classInfo = ClassInfo.of(dataClass);
for (String name : new TreeSet(classInfo.getNames())) {
FieldInfo fieldInfo = classInfo.getFieldInfo(name);
if (fieldInfo.isFinal()) {
continue;
}
if (++numFields[0] != 1) {
fieldsBuf.append(',');
}
fieldsBuf.append(name);
// TODO(yanivi): handle Java arrays?
Class> fieldClass = fieldInfo.getType();
if (Collection.class.isAssignableFrom(fieldClass)) {
// TODO(yanivi): handle Java collection of Java collection or Java map?
fieldClass = (Class>) Types.getIterableParameter(fieldInfo.getField().getGenericType());
}
// TODO(yanivi): implement support for map when server implements support for *:*
if (fieldClass != null) {
if (fieldInfo.isPrimitive()) {
if (name.charAt(0) != '@' && !name.equals("text()")) {
// TODO(yanivi): wait for bug fix from server to support text() -- already fixed???
// buf.append("/text()");
}
} else if (!Collection.class.isAssignableFrom(fieldClass)
&& !Map.class.isAssignableFrom(fieldClass)) {
int[] subNumFields = new int[1];
int openParenIndex = fieldsBuf.length();
fieldsBuf.append('(');
// TODO(yanivi): abort if found cycle to avoid infinite loop
appendFieldsFor(fieldsBuf, fieldClass, subNumFields);
updateFieldsBasedOnNumFields(fieldsBuf, openParenIndex, subNumFields[0]);
}
}
}
}
private static void appendFeedFields(
StringBuilder fieldsBuf, Class> feedClass, Class> entryClass) {
int[] numFields = new int[1];
appendFieldsFor(fieldsBuf, feedClass, numFields);
if (numFields[0] != 0) {
fieldsBuf.append(",");
}
fieldsBuf.append("entry(");
int openParenIndex = fieldsBuf.length() - 1;
numFields[0] = 0;
appendFieldsFor(fieldsBuf, entryClass, numFields);
updateFieldsBasedOnNumFields(fieldsBuf, openParenIndex, numFields[0]);
}
private static void updateFieldsBasedOnNumFields(
StringBuilder fieldsBuf, int openParenIndex, int numFields) {
switch (numFields) {
case 0:
fieldsBuf.deleteCharAt(openParenIndex);
break;
case 1:
fieldsBuf.setCharAt(openParenIndex, '/');
break;
default:
fieldsBuf.append(')');
}
}
/**
* Compute the patch object of key/value pairs from the given original and patched objects, adding
* a {@code @gd:fields} key for the fields mask.
*
* @param patched patched object
* @param original original object
* @return patch object of key/value pairs
*/
public static Map computePatch(Object patched, Object original) {
FieldsMask fieldsMask = new FieldsMask();
ArrayMap result = computePatchInternal(fieldsMask, patched, original);
if (fieldsMask.numDifferences != 0) {
result.put("@gd:fields", fieldsMask.buf.toString());
}
return result;
}
private static ArrayMap computePatchInternal(
FieldsMask fieldsMask, Object patchedObject, Object originalObject) {
ArrayMap result = ArrayMap.create();
Map patchedMap = Data.mapOf(patchedObject);
Map originalMap = Data.mapOf(originalObject);
TreeSet fieldNames = new TreeSet();
fieldNames.addAll(patchedMap.keySet());
fieldNames.addAll(originalMap.keySet());
for (String name : fieldNames) {
Object originalValue = originalMap.get(name);
Object patchedValue = patchedMap.get(name);
if (originalValue == patchedValue) {
continue;
}
Class> type = originalValue == null ? patchedValue.getClass() : originalValue.getClass();
if (Data.isPrimitive(type)) {
if (originalValue != null && originalValue.equals(patchedValue)) {
continue;
}
fieldsMask.append(name);
// TODO(yanivi): wait for bug fix from server
// if (!name.equals("text()") && name.charAt(0) != '@') {
// fieldsMask.buf.append("/text()");
// }
if (patchedValue != null) {
result.add(name, patchedValue);
}
} else if (Collection.class.isAssignableFrom(type)) {
if (originalValue != null && patchedValue != null) {
@SuppressWarnings("unchecked")
Collection