/*
* Copyright (c) 2014 LinkedIn Corp.
*
* 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.linkedin.data.transform;
import com.linkedin.data.DataMap;
import com.linkedin.data.Null;
import com.linkedin.data.schema.PathSpec;
import com.linkedin.data.transform.filter.CopyFilter;
import com.linkedin.data.transform.filter.request.MaskTree;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Keren Jin
*/
public class ProjectionUtil
{
private static class PathSpecFilter extends CopyFilter
{
/**
* The only error supposed to be received here is where the value is primitive but filter is complex
* This should be ignored to allow a prefix to stay.
*/
@Override
protected Object onError(Object field, String format, Object... args)
{
return null;
}
}
/**
* <p>Given a {@link MaskTree} and a {@link PathSpec}, return if the PathSpec is present.
* In other words, it tells that when Rest.li projects on a record with the given MaskTree, whether the field
* represented by the PathSpec would survive.</p>
*
* Convenient version of getPresentPaths() to only test one {@link PathSpec} at a time.
* For more details, check the documentation of getPresentPaths().
*
* @param filter filter to apply
* @param path PathSpec to test
* @return true if the PathSpec is present in the MaskTree filtering
*/
public static boolean isPathPresent(MaskTree filter, PathSpec path)
{
return !getPresentPaths(filter, Collections.singleton(path)).isEmpty();
}
/**
* <p>Given a {@link MaskTree} and a {@link Set} of {@link PathSpec}s, return the PathSpecs that are present.
* In other words, it tells that when Rest.li projects on a record with the given MaskTree, whether the fields
* represented by the PathSpecs would survive.</p>
*
* <p>If a PathSpec is a prefix of the filter or the filter is a prefix of a PathSpec, it is always considered present. For example,
* <pre>
* MaskTree: /foo/bar: POSITIVE
* PathSpecs: /foo: present
* /foo/bar: present
* /foo/bar/baz: present
* /xyz: not present
* </pre>
* </p>
*
* <p>Empty filter Empty PathSpec is considered prefix of all filters.</p>
*
* @param filter filter to apply
* @param paths PathSpecs to test
* @return PathSpecs that are present in the MaskTree filtering
*/
public static Set<PathSpec> getPresentPaths(MaskTree filter, Set<PathSpec> paths)
{
// this emulates the behavior of Rest.li server
// if client does not specify any mask, the server receives null when retrieving the MaskTree
// in this case, all fields are returned
if (filter == null)
{
return paths;
}
final DataMap filterMap = filter.getDataMap();
if (filter.getDataMap().isEmpty())
{
return Collections.emptySet();
}
final DataMap pathSpecMap = createPathSpecMap(paths);
@SuppressWarnings("unchecked")
final DataMap filteredPathSpecs = (DataMap) new PathSpecFilter().filter(pathSpecMap, filterMap);
return validate(filteredPathSpecs, paths);
}
private static DataMap createPathSpecMap(Set<PathSpec> paths)
{
final DataMap pathSpecMap = new DataMap();
for (PathSpec p : paths)
{
final List<String> components = p.getPathComponents();
DataMap currentMap = pathSpecMap;
for (int i = 0; i < components.size(); ++i)
{
final String currentComponent = components.get(i);
final Object currentValue = currentMap.get(currentComponent);
if (i < components.size() - 1)
{
if (currentValue instanceof DataMap)
{
@SuppressWarnings("unchecked")
final DataMap valueMap = (DataMap) currentValue;
currentMap = valueMap;
}
else
{
final DataMap newMap = new DataMap();
currentMap.put(currentComponent, newMap);
currentMap = newMap;
}
}
else if (currentValue == null)
{
currentMap.put(currentComponent, Null.getInstance());
}
}
}
return pathSpecMap;
}
private static Set<PathSpec> validate(DataMap filteredPathSpecs, Set<PathSpec> paths)
{
final Set<PathSpec> result = new HashSet<PathSpec>();
for (PathSpec p : paths)
{
final List<String> components = p.getPathComponents();
DataMap currentMap = filteredPathSpecs;
boolean isPresent = true;
for (int i = 0; i < components.size(); ++i)
{
final String currentComponent = components.get(i);
final Object currentValue = currentMap.get(currentComponent);
if (currentValue instanceof DataMap)
{
@SuppressWarnings("unchecked")
final DataMap valueMap = (DataMap) currentValue;
currentMap = valueMap;
}
else
{
isPresent = currentMap.containsKey(currentComponent);
break;
}
}
if (isPresent)
{
result.add(p);
}
}
return result;
}
}