/*
 * Copyright 2017-2018 the original author or 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
 *
 *      https://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.springframework.cloud.gcp.data.datastore.repository.query;

import java.lang.reflect.Method;
import java.util.Optional;

import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.lang.Nullable;

/**
 * A metadata class for Query Methods for Spring Data Cloud Datastore.
 *
 * @author Chengyuan Zhao
 *
 * @since 1.1
 */
public class DatastoreQueryMethod extends QueryMethod {

	private final Method method;

	/**
	 * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct
	 * query to use for following invocations of the method given.
	 *
	 * @param method must not be {@literal null}.
	 * @param metadata must not be {@literal null}.
	 * @param factory must not be {@literal null}.
	 */
	DatastoreQueryMethod(Method method, RepositoryMetadata metadata,
			ProjectionFactory factory) {
		super(method, metadata, factory);
		this.method = method;
	}

	/**
	 * Returns whether the method has an annotated query.
	 *
	 * @return true if this query method has annotation that holds the query string.
	 */
	boolean hasAnnotatedQuery() {
		return getQueryAnnotation() != null;
	}

	/**
	 * Get the type of the collection of the returned object.
	 * @return the collection type of returned object if it is collection-like.
	 * {@code null} otherwise
	 */
	Class getCollectionReturnType() {
		return isCollectionQuery() ? this.method.getReturnType() : null;
	}

	/**
	 * Return whether this method is an exists query.
	 *
	 * @return true if this query method is an exists query, and false otherwise.
	 */
	boolean isExistsQuery() {
		Query annotation = getQueryAnnotation();
		return annotation != null && annotation.exists();
	}

	/**
	 * Return whether this method is a count query.
	 *
	 * @return true if this query method is a count query, and false otherwise.
	 */
	boolean isCountQuery() {
		Query annotation = getQueryAnnotation();
		return annotation != null && annotation.count();
	}

	/**
	 * Returns the {@link Query} annotation that is applied to the method or {@code null}
	 * if none available.
	 *
	 * @return the query annotation that is applied.
	 */
	@Nullable
	Query getQueryAnnotation() {
		return AnnotatedElementUtils.findMergedAnnotation(this.method, Query.class);
	}


	boolean isNullable() {
		return AnnotatedElementUtils.findMergedAnnotation(this.method, Nullable.class) != null;
	}

	boolean isOptionalReturnType() {
		return this.method.getReturnType().isAssignableFrom(Optional.class);
	}
}

@@ -709,22 +709,87 @@
 ang.
-reflect.Method
+annotation.Annotation;%0Aimport java.lang.reflect.Method;%0Aimport java.util.Arrays
 ;%0Aim
@@ -1328,16 +1328,53 @@
 ethod;%0A%0A
+%09private Boolean isNullable = null;%0A%0A
 %09/**%0A%09 *
@@ -3202,94 +3202,267 @@
 %7B%0A%09%09
-return AnnotatedElementUtils.findMergedAnnotation(this.method, Nullable.class) != null
+if (this.isNullable == null) %7B%0A%09%09%09Optional%3CAnnotation%3E nullable = Arrays.stream(this.method.getAnnotations())%0A%09%09%09%09%09.filter(annotation -%3E annotation instanceof Nullable)%0A%09%09%09%09%09.findFirst();%0A%09%09%09this.isNullable = nullable.isPresent();%0A%09%09%7D%0A%09%09return this.isNullable
 ;%0A%09%7D

Fix Datastore CI (#1577)