
ca.uhn.fhir.rest.method.BaseMethodBinding Maven / Gradle / Ivy
package ca.uhn.fhir.rest.method;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2016 University Health Network
* %%
* 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.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.server.BundleProviders;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ReflectionUtil;
public abstract class BaseMethodBinding implements IClientResponseHandler {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
private FhirContext myContext;
private Method myMethod;
private List myParameters;
private Object myProvider;
private boolean mySupportsConditional;
private boolean mySupportsConditionalMultiple;
public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
assert theMethod != null;
assert theContext != null;
myMethod = theMethod;
myContext = theContext;
myProvider = theProvider;
myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType());
for (IParameter next : myParameters) {
if (next instanceof ConditionalParamBinder) {
mySupportsConditional = true;
if (((ConditionalParamBinder) next).isSupportsMultiple()) {
mySupportsConditionalMultiple = true;
}
break;
}
}
}
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List> thePreferTypes) {
EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType);
if (encoding == null) {
NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
populateException(ex, theResponseReader);
throw ex;
}
IParser parser = encoding.newParser(getContext());
parser.setPreferTypes(thePreferTypes);
return parser;
}
protected IParser createAppropriateParserForParsingServerRequest(RequestDetails theRequest) {
String contentTypeHeader = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
EncodingEnum encoding;
if (isBlank(contentTypeHeader)) {
encoding = EncodingEnum.XML;
} else {
int semicolon = contentTypeHeader.indexOf(';');
if (semicolon != -1) {
contentTypeHeader = contentTypeHeader.substring(0, semicolon);
}
encoding = EncodingEnum.forContentType(contentTypeHeader);
}
if (encoding == null) {
throw new InvalidRequestException("Request contins non-FHIR conent-type header value: " + contentTypeHeader);
}
IParser parser = encoding.newParser(getContext());
return parser;
}
protected Object[] createParametersForServerRequest(RequestDetails theRequest) {
Object[] params = new Object[getParameters().size()];
for (int i = 0; i < getParameters().size(); i++) {
IParameter param = getParameters().get(i);
if (param == null) {
continue;
}
params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
}
return params;
}
public List> getAllowableParamAnnotations() {
return null;
}
public FhirContext getContext() {
return myContext;
}
public Set getIncludes() {
Set retVal = new TreeSet();
for (IParameter next : myParameters) {
if (next instanceof IncludeParameter) {
retVal.addAll(((IncludeParameter) next).getAllow());
}
}
return retVal;
}
public Method getMethod() {
return myMethod;
}
public List getParameters() {
return myParameters;
}
public Object getProvider() {
return myProvider;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Set getRequestIncludesFromParams(Object[] params) {
if (params == null || params.length == 0) {
return null;
}
int index = 0;
boolean match = false;
for (IParameter parameter : myParameters) {
if (parameter instanceof IncludeParameter) {
match = true;
break;
}
index++;
}
if (!match) {
return null;
}
if (index >= params.length) {
ourLog.warn("index out of parameter range (should never happen");
return null;
}
if (params[index] instanceof Set) {
return (Set) params[index];
}
if (params[index] instanceof Iterable) {
Set includes = new HashSet();
for (Object o : (Iterable) params[index]) {
if (o instanceof Include) {
includes.add(o);
}
}
return includes;
}
ourLog.warn("include params wasn't Set or Iterable, it was {}", params[index].getClass());
return null;
}
/**
* Returns the name of the resource this method handles, or null
if this method is not resource specific
*/
public abstract String getResourceName();
public abstract RestOperationTypeEnum getRestOperationType();
/**
* Determine which operation is being fired for a specific request
*
* @param theRequestDetails
* The request
*/
public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
return getRestOperationType();
}
public abstract boolean incomingServerRequestMatchesMethod(RequestDetails theRequest);
public abstract BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException;
public abstract Object invokeServer(IRestfulServer> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException;
protected final Object invokeServerMethod(IRestfulServer> theServer, RequestDetails theRequest, Object[] theMethodParams) {
// Handle server action interceptors
RestOperationTypeEnum operationType = getRestOperationType(theRequest);
if (operationType != null) {
for (IServerInterceptor next : theServer.getInterceptors()) {
ActionRequestDetails details = new ActionRequestDetails(theRequest);
populateActionRequestDetailsForInterceptor(theRequest, details, theMethodParams);
next.incomingRequestPreHandled(operationType, details);
}
}
// Actually invoke the method
try {
Method method = getMethod();
return method.invoke(getProvider(), theMethodParams);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof BaseServerResponseException) {
throw (BaseServerResponseException) e.getCause();
} else {
throw new InternalErrorException("Failed to call access method", e);
}
} catch (Exception e) {
throw new InternalErrorException("Failed to call access method", e);
}
}
/**
* Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally.
*/
public boolean isSupportsConditional() {
return mySupportsConditional;
}
/**
* Does this method support conditional operations over multiple objects (basically for conditional delete)
*/
public boolean isSupportsConditionalMultiple() {
return mySupportsConditionalMultiple;
}
/**
* Subclasses may override this method (but should also call super.{@link #populateActionRequestDetailsForInterceptor(RequestDetails, ActionRequestDetails, Object[])} to provide method specifics to the
* interceptors.
*
* @param theRequestDetails
* The server request details
* @param theDetails
* The details object to populate
* @param theMethodParams
* The method params as generated by the specific method binding
*/
protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
// nothing by default
}
protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) {
BaseServerResponseException ex;
switch (theStatusCode) {
case Constants.STATUS_HTTP_400_BAD_REQUEST:
ex = new InvalidRequestException("Server responded with HTTP 400");
break;
case Constants.STATUS_HTTP_404_NOT_FOUND:
ex = new ResourceNotFoundException("Server responded with HTTP 404");
break;
case Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED:
ex = new MethodNotAllowedException("Server responded with HTTP 405");
break;
case Constants.STATUS_HTTP_409_CONFLICT:
ex = new ResourceVersionConflictException("Server responded with HTTP 409");
break;
case Constants.STATUS_HTTP_412_PRECONDITION_FAILED:
ex = new PreconditionFailedException("Server responded with HTTP 412");
break;
case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theStatusCode, null);
// TODO: handle if something other than OO comes back
BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseReader);
ex = new UnprocessableEntityException(myContext, operationOutcome);
break;
default:
ex = new UnclassifiedServerFailureException(theStatusCode, "Server responded with HTTP " + theStatusCode);
break;
}
populateException(ex, theResponseReader);
return ex;
}
/** For unit tests only */
public void setParameters(List theParameters) {
myParameters = theParameters;
}
protected IBundleProvider toResourceList(Object response) throws InternalErrorException {
if (response == null) {
return BundleProviders.newEmptyList();
} else if (response instanceof IBundleProvider) {
return (IBundleProvider) response;
} else if (response instanceof IBaseResource) {
return BundleProviders.newList((IBaseResource) response);
} else if (response instanceof Collection) {
List retVal = new ArrayList();
for (Object next : ((Collection>) response)) {
retVal.add((IBaseResource) next);
}
return BundleProviders.newList(retVal);
} else if (response instanceof MethodOutcome) {
IBaseResource retVal = ((MethodOutcome) response).getOperationOutcome();
if (retVal == null) {
retVal = getContext().getResourceDefinition("OperationOutcome").newInstance();
}
return BundleProviders.newList(retVal);
} else {
throw new InternalErrorException("Unexpected return type: " + response.getClass().getCanonicalName());
}
}
@SuppressWarnings("unchecked")
public static BaseMethodBinding> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
Read read = theMethod.getAnnotation(Read.class);
Search search = theMethod.getAnnotation(Search.class);
Metadata conformance = theMethod.getAnnotation(Metadata.class);
Create create = theMethod.getAnnotation(Create.class);
Update update = theMethod.getAnnotation(Update.class);
Delete delete = theMethod.getAnnotation(Delete.class);
History history = theMethod.getAnnotation(History.class);
Validate validate = theMethod.getAnnotation(Validate.class);
GetTags getTags = theMethod.getAnnotation(GetTags.class);
AddTags addTags = theMethod.getAnnotation(AddTags.class);
DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
Transaction transaction = theMethod.getAnnotation(Transaction.class);
Operation operation = theMethod.getAnnotation(Operation.class);
GetPage getPage = theMethod.getAnnotation(GetPage.class);
Patch patch = theMethod.getAnnotation(Patch.class);
// ** if you add another annotation above, also add it to the next line:
if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation, getPage, patch)) {
return null;
}
if (getPage != null) {
return new PageMethodBinding(theContext, theMethod);
}
Class extends IBaseResource> returnType;
Class extends IBaseResource> returnTypeFromRp = null;
if (theProvider instanceof IResourceProvider) {
returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
+ toLogString(returnTypeFromRp) + " - Must return a resource type");
}
}
Class> returnTypeFromMethod = theMethod.getReturnType();
if (getTags != null) {
if (!TagList.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @"
+ GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
}
} else if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
// returns a method outcome
} else if (IBundleProvider.class.equals(returnTypeFromMethod)) {
// returns a bundle provider
} else if (Bundle.class.equals(returnTypeFromMethod)) {
// returns a bundle
} else if (void.class.equals(returnTypeFromMethod)) {
// returns a bundle
} else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (returnTypeFromMethod == null) {
ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod);
} else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !isResourceInterface(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns a collection with generic type " + toLogString(returnTypeFromMethod)
+ " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List or List )");
}
} else {
if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, " + Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName()
+ ", etc., see the documentation for more details)");
}
}
Class extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
if (read != null) {
returnTypeFromAnnotation = read.type();
} else if (search != null) {
returnTypeFromAnnotation = search.type();
} else if (history != null) {
returnTypeFromAnnotation = history.type();
} else if (delete != null) {
returnTypeFromAnnotation = delete.type();
} else if (patch != null) {
returnTypeFromAnnotation = patch.type();
} else if (create != null) {
returnTypeFromAnnotation = create.type();
} else if (update != null) {
returnTypeFromAnnotation = update.type();
} else if (validate != null) {
returnTypeFromAnnotation = validate.type();
} else if (getTags != null) {
returnTypeFromAnnotation = getTags.type();
} else if (addTags != null) {
returnTypeFromAnnotation = addTags.type();
} else if (deleteTags != null) {
returnTypeFromAnnotation = deleteTags.type();
}
if (returnTypeFromRp != null) {
if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) {
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
+ returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
}
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException(
"Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName()
+ " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
}
returnType = returnTypeFromAnnotation;
} else {
returnType = returnTypeFromRp;
}
} else {
if (!isResourceInterface(returnTypeFromAnnotation)) {
if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type");
}
returnType = returnTypeFromAnnotation;
} else {
// if (IRestfulClient.class.isAssignableFrom(theMethod.getDeclaringClass())) {
// Clients don't define their methods in resource specific types, so they can
// infer their resource type from the method return type.
returnType = (Class extends IBaseResource>) returnTypeFromMethod;
// } else {
// This is a plain provider method returning a resource, so it should be
// an operation or global search presumably
// returnType = null;
}
}
if (read != null) {
return new ReadMethodBinding(returnType, theMethod, theContext, theProvider);
} else if (search != null) {
if (search.dynamic()) {
IDynamicSearchResourceProvider provider = (IDynamicSearchResourceProvider) theProvider;
return new DynamicSearchMethodBinding(returnType, theMethod, theContext, provider);
} else {
return new SearchMethodBinding(returnType, theMethod, theContext, theProvider);
}
} else if (conformance != null) {
return new ConformanceMethodBinding(theMethod, theContext, theProvider);
} else if (create != null) {
return new CreateMethodBinding(theMethod, theContext, theProvider);
} else if (update != null) {
return new UpdateMethodBinding(theMethod, theContext, theProvider);
} else if (delete != null) {
return new DeleteMethodBinding(theMethod, theContext, theProvider);
} else if (patch != null) {
return new PatchMethodBinding(theMethod, theContext, theProvider);
} else if (history != null) {
return new HistoryMethodBinding(theMethod, theContext, theProvider);
} else if (validate != null) {
if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) {
return new ValidateMethodBindingDstu1(theMethod, theContext, theProvider);
} else {
return new ValidateMethodBindingDstu2Plus(returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate);
}
} else if (getTags != null) {
return new GetTagsMethodBinding(theMethod, theContext, theProvider, getTags);
} else if (addTags != null) {
return new AddTagsMethodBinding(theMethod, theContext, theProvider, addTags);
} else if (deleteTags != null) {
return new DeleteTagsMethodBinding(theMethod, theContext, theProvider, deleteTags);
} else if (transaction != null) {
return new TransactionMethodBinding(theMethod, theContext, theProvider);
} else if (operation != null) {
return new OperationMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation);
} else {
throw new ConfigurationException("Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
}
// // each operation name must have a request type annotation and be
// unique
// if (null != read) {
// return rm;
// }
//
// SearchMethodBinding sm = new SearchMethodBinding();
// if (null != search) {
// sm.setRequestType(SearchMethodBinding.RequestType.GET);
// } else if (null != theMethod.getAnnotation(PUT.class)) {
// sm.setRequestType(SearchMethodBinding.RequestType.PUT);
// } else if (null != theMethod.getAnnotation(POST.class)) {
// sm.setRequestType(SearchMethodBinding.RequestType.POST);
// } else if (null != theMethod.getAnnotation(DELETE.class)) {
// sm.setRequestType(SearchMethodBinding.RequestType.DELETE);
// } else {
// return null;
// }
//
// return sm;
}
private static boolean isResourceInterface(Class> theReturnTypeFromMethod) {
return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class);
}
private static void populateException(BaseServerResponseException theEx, Reader theResponseReader) {
try {
String responseText = IOUtils.toString(theResponseReader);
theEx.setResponseBody(responseText);
} catch (IOException e) {
ourLog.debug("Failed to read response", e);
}
}
private static String toLogString(Class> theType) {
if (theType == null) {
return null;
}
return theType.getCanonicalName();
}
private static boolean verifyIsValidResourceReturnType(Class> theReturnType) {
if (theReturnType == null) {
return false;
}
if (!IBaseResource.class.isAssignableFrom(theReturnType)) {
return false;
}
return true;
// boolean retVal = Modifier.isAbstract(theReturnType.getModifiers()) == false;
// return retVal;
}
public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) {
Object obj1 = null;
for (Object object : theAnnotations) {
if (object != null) {
if (obj1 == null) {
obj1 = object;
} else {
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
+ obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
}
}
}
if (obj1 == null) {
return false;
// throw new ConfigurationException("Method '" +
// theNextMethod.getName() + "' on type '" +
// theNextMethod.getDeclaringClass().getSimpleName() +
// " has no FHIR method annotations.");
}
return true;
}
/**
* @see ServletRequestDetails#getByteStreamRequestContents()
*/
public static class ActiveRequestReader implements IRequestReader {
@Override
public InputStream getInputStream(RequestDetails theRequestDetails) throws IOException {
return theRequestDetails.getInputStream();
}
}
/**
* @see ServletRequestDetails#getByteStreamRequestContents()
*/
public static class InactiveRequestReader implements IRequestReader {
@Override
public InputStream getInputStream(RequestDetails theRequestDetails) {
throw new IllegalStateException("The servlet-api JAR is not found on the classpath. Please check that this library is available.");
}
}
/**
* @see ServletRequestDetails#getByteStreamRequestContents()
*/
public static interface IRequestReader {
InputStream getInputStream(RequestDetails theRequestDetails) throws IOException;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy