/* * Copyright 2000-2018 JetBrains s.r.o. * * 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 jetbrains.buildServer.server.rest.data; import com.intellij.openapi.diagnostic.Logger; import java.util.*; import jetbrains.buildServer.server.rest.errors.OperationException; import jetbrains.buildServer.server.rest.model.PagerData; import jetbrains.buildServer.util.CollectionsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Yegor.Yarko * Date: 15/06/2015 */ public class GraphFinder extends AbstractFinder { private static final Logger LOG = Logger.getInstance(GraphFinder.class.getName()); protected static final String DIMENSION_FROM = "from"; protected static final String DIMENSION_TO = "to"; protected static final String DIMENSION_STOP = "stop"; protected static final String DIMENSION_RECURSIVE = "recursive"; protected static final String DIMENSION_INCLUDE_INITIAL = "includeInitial"; private final Finder myFinder; @NotNull private final Traverser myTraverser; private Long myDefaultLookupLimit; public GraphFinder(@NotNull Finder finder, @NotNull Traverser traverser) { super(new String[]{DIMENSION_FROM, DIMENSION_TO, DIMENSION_RECURSIVE, DIMENSION_INCLUDE_INITIAL, DIMENSION_STOP}); myFinder = finder; myTraverser = traverser; } @NotNull @Override public String getItemLocator(@NotNull final T t) { throw new OperationException("getItemLocator is not implemented in GraphFinder"); } @NotNull @Override public ItemFilter getFilter(@NotNull final Locator locator) { return new MultiCheckerFilter(); } @Nullable @Override public Long getDefaultLookupLimit() { return myDefaultLookupLimit; } public void setDefaultLookupLimit(final Long defaultLookupLimit) { myDefaultLookupLimit = defaultLookupLimit; } @NotNull @Override public ItemHolder getPrefilteredItems(@NotNull final Locator locator) { boolean recursive = locator.getSingleDimensionValueAsStrictBoolean(DIMENSION_RECURSIVE, true); boolean includeOriginal = locator.getSingleDimensionValueAsStrictBoolean(DIMENSION_INCLUDE_INITIAL, false); final List toItems = getItemsFromDimension(locator, DIMENSION_TO); final List fromItems = getItemsFromDimension(locator, DIMENSION_FROM); final List stopItems = getItemsFromDimension(locator, DIMENSION_STOP); Long lookupLimit = locator.getSingleDimensionValueAsLong(DIMENSION_LOOKUP_LIMIT, getDefaultLookupLimit()); Set resultTo = new LinkedHashSet(); if (!toItems.isEmpty()) { if (includeOriginal) { resultTo.addAll(toItems); } collectLinked(resultTo, toItems, CollectionsUtil.join(fromItems, stopItems), lookupLimit, myTraverser.getChildren(), recursive); } Set resultFrom = new LinkedHashSet(); if (!fromItems.isEmpty()) { if (includeOriginal) { resultFrom.addAll(fromItems); } collectLinked(resultFrom, fromItems, CollectionsUtil.join(toItems, stopItems), lookupLimit, myTraverser.getParents(), recursive); } ArrayList result; if (!resultTo.isEmpty() && !resultFrom.isEmpty()) { result = new ArrayList(CollectionsUtil.intersect(resultTo, resultFrom)); } else { result = new ArrayList(!resultTo.isEmpty() ? resultTo : resultFrom); } return getItemHolder(result); //can improve performance by performing traversing in the holder, not retrieving all upfront, this will also add support for lookupLimit } @NotNull private List getItemsFromDimension(@NotNull final Locator locator, @NotNull final String dimensionName) { final List dimensionValues = locator.getDimensionValue(dimensionName); if (!dimensionValues.isEmpty()) { final ArrayList result = new ArrayList(); for (String dimensionValue : dimensionValues) { result.addAll(myFinder.getItems(dimensionValue).myEntries); } return result; } return Collections.emptyList(); } protected void collectLinked(@NotNull final Set result, @NotNull Collection toProcess, @NotNull Collection stopItems, final Long lookupLimit, @NotNull LinkRetriever linkRetriever, final boolean recursive) { final int initialSize = result.size(); while (!toProcess.isEmpty()) { Set linkedItems = new LinkedHashSet(); for (T item : toProcess) { if (stopItems.contains(item)) { result.add(item); } else { final List linked = linkRetriever.getLinked(item); result.addAll(linked); linkedItems.addAll(linked); } } toProcess = linkedItems; if (!recursive) break; if (lookupLimit != null && (result.size() - initialSize >= lookupLimit)) { LOG.debug("Hit lookupLimit " + lookupLimit + " while traversing graph, result is partial"); break; } } } public interface Traverser { /** * Get items when "to" is specified */ @NotNull LinkRetriever getChildren(); /** * Get items when "from" is specified */ @NotNull LinkRetriever getParents(); } public interface LinkRetriever { @NotNull List getLinked(@NotNull S item); } @NotNull public ParsedLocator getParsedLocator(@NotNull String locatorText) { Locator locator = createLocator(locatorText, null); return new ParsedLocator() { @Nullable @Override public Integer getCount() { Long result = locator.getSingleDimensionValueAsLong(PagerData.COUNT); return result == null ? null : result.intValue(); } @NotNull @Override public List getFromItems() { return getItemsFromDimension(locator, DIMENSION_FROM); //should be lazy as it might be never needed based on certain count values } @Override public boolean isRecursive() { return locator.getSingleDimensionValueAsStrictBoolean(DIMENSION_RECURSIVE, true); } @Override public boolean isIncludeInitial() { return locator.getSingleDimensionValueAsStrictBoolean(DIMENSION_INCLUDE_INITIAL, false); } @Override public boolean isAllDimensionsUsed() { return locator.getUnusedDimensions().isEmpty(); } }; } public interface ParsedLocator { @Nullable Integer getCount(); @NotNull List getFromItems(); boolean isRecursive(); boolean isIncludeInitial(); boolean isAllDimensionsUsed(); } }