/* 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.restli.examples.greetings.server; import com.linkedin.common.callback.Callback; import com.linkedin.data.template.StringArray; import com.linkedin.parseq.BaseTask; import com.linkedin.parseq.Task; import com.linkedin.parseq.promise.Promise; import com.linkedin.parseq.promise.Promises; import com.linkedin.parseq.promise.SettablePromise; import com.linkedin.restli.common.HttpStatus; import com.linkedin.restli.examples.greetings.api.Empty; import com.linkedin.restli.examples.greetings.api.Greeting; import com.linkedin.restli.examples.greetings.api.SearchMetadata; import com.linkedin.restli.examples.greetings.api.Tone; import com.linkedin.restli.server.ActionResult; import com.linkedin.restli.server.BatchCreateRequest; import com.linkedin.restli.server.BatchCreateResult; import com.linkedin.restli.server.BatchDeleteRequest; import com.linkedin.restli.server.BatchPatchRequest; import com.linkedin.restli.server.BatchResult; import com.linkedin.restli.server.BatchUpdateRequest; import com.linkedin.restli.server.BatchUpdateResult; import com.linkedin.restli.server.CollectionResult; import com.linkedin.restli.server.CreateResponse; import com.linkedin.restli.server.PagingContext; import com.linkedin.restli.server.RestLiServiceException; import com.linkedin.restli.server.UpdateResponse; import com.linkedin.restli.server.annotations.Action; import com.linkedin.restli.server.annotations.CallbackParam; import com.linkedin.restli.server.annotations.Finder; import com.linkedin.restli.server.annotations.PagingContextParam; import com.linkedin.restli.server.annotations.QueryParam; import com.linkedin.restli.server.annotations.RestLiCollection; import com.linkedin.restli.server.annotations.RestMethod; import com.linkedin.restli.server.resources.CollectionResourceTemplate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * Tests to observe restli's resilience for resource methods returning null. We are simply reusing * the Greetings model here for our own null-generating purposes. * * @author Karim Vidhani */ @RestLiCollection(name = "nullGreeting", namespace = "com.linkedin.restli.examples.greetings.client") public class NullGreetingsResourceImpl extends CollectionResourceTemplate<Long, Greeting> { private static final String[] GREETINGS = {"Good morning!", "Guten Morgen!", "Buenos dias!", "Bon jour!", "Buon Giorno!"}; private static final Tone[] TONES = {Tone.FRIENDLY, Tone.SINCERE, Tone.INSULTING}; private static final int INITIAL_SIZE = 20; private static final String[] INITIAL_MESSAGES = new String[INITIAL_SIZE]; private static final Tone[] INITIAL_TONES = new Tone[INITIAL_SIZE]; private static Long ID_SEQ = 0l; private static final Map<Long, Greeting> DB = new HashMap<Long, Greeting>(); private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); private static final int DELAY = 100; static { // generate some "random" initial data for (int i = 0; i < INITIAL_SIZE; i++) { INITIAL_MESSAGES[i] = GREETINGS[i % GREETINGS.length]; } for (int i = 0; i < INITIAL_SIZE; i++) { INITIAL_TONES[i] = TONES[i % TONES.length]; } for (int i = 0; i < INITIAL_SIZE; i++) { Greeting g = new Greeting().setId(ID_SEQ++).setMessage(INITIAL_MESSAGES[i]).setTone(INITIAL_TONES[i]); DB.put(g.getId(), g); } } public NullGreetingsResourceImpl() { } @RestMethod.Create public CreateResponse create(Greeting entity) { //Based off of the message in the greeting, we send back various types of nulls if (entity.getMessage().equalsIgnoreCase("nullCreateResponse")) { //Return a null CreateResponse return null; } else { //Return a valid CreateResponse but with a null HttpStatus final HttpStatus nullStatus = null; return new CreateResponse(nullStatus); } //Note, we don't need a test for returning a null entityID } @Finder("searchReturnNullList") public List<Greeting> searchReturnNullList(@PagingContextParam PagingContext ctx, @QueryParam("tone") Tone tone) { if (tone == Tone.INSULTING) { //return a null list return null; } else { //return a list with a null element in it final List<Greeting> greetings = new ArrayList<Greeting>(); greetings.add(null); greetings.add(DB.get(1)); return greetings; } } @Finder("searchReturnNullCollectionList") public CollectionResult<Greeting, SearchMetadata> searchReturnNullCollectionList(@PagingContextParam PagingContext ctx, @QueryParam("tone") Tone tone) { if (tone == Tone.INSULTING) { //return a null CollectionResult return null; } else if (tone == Tone.SINCERE) { //return a CollectionResult with a null list return new CollectionResult<Greeting, SearchMetadata>(null); } else { //return a CollectionResult with a list that has a null element in it final List<Greeting> greetings = new ArrayList<Greeting>(); greetings.add(null); greetings.add(DB.get(1)); return new CollectionResult<Greeting, SearchMetadata>(greetings); } } @RestMethod.Get public Greeting get(Long key) { return null; } @RestMethod.GetAll public CollectionResult<Greeting, Empty> getAllCollectionResult(@PagingContextParam PagingContext ctx) { return null; } @RestMethod.BatchGet public BatchResult<Long, Greeting> batchGetBatchResult(Set<Long> ids) { final Map<Long, Greeting> greetingMap = new HashMap<Long, Greeting>(); greetingMap.put(0l, DB.get(0l)); if (ids.contains(1l)) { //Return null BatchResult return null; } else if (ids.contains(2l)) { //Return BatchResult with null maps return new BatchResult<Long, Greeting>(null, null, null); } else if (ids.contains(3l)) { //Return a BatchResult with a null key in the status map. final Map<Long, HttpStatus> statusMap = new HashMap<Long, HttpStatus>(); statusMap.put(null, null); return new BatchResult<Long, Greeting>(greetingMap, statusMap, null); } else if (ids.contains(4l)) { //Return a BatchResult that has a map with a null key. greetingMap.put(null, null); return new BatchResult<Long, Greeting>(greetingMap, null, null); } else { /* * Return a BatchResult with java.util.concurrent.ConcurrentHashMaps. * This test is in place because certain map implementations, such as ConcurrentHashMap, can throw an NPE when * calling contains(null). We want to verify that checking for the existence of nulls in maps returned by * Rest.li resource methods do not cause such NPEs. * This is one of the few cases in this file where an error will not be generated by Rest.li. */ final Map<Long, Greeting> concurrentGreetingMap = new ConcurrentHashMap<Long, Greeting>(greetingMap); return new BatchResult<Long, Greeting>(concurrentGreetingMap, new ConcurrentHashMap<Long, HttpStatus>(), new ConcurrentHashMap<Long, RestLiServiceException>()); } } @RestMethod.Update public UpdateResponse update(Long key, Greeting entity) { if (key == 1l) { //Return null UpdateResponse return null; } else { //Return an UpdateResponse with a null HttpStatus return new UpdateResponse(null); } } @RestMethod.BatchCreate public BatchCreateResult<Long, Greeting> batchCreate(BatchCreateRequest<Long, Greeting> entities) { List<CreateResponse> responses = new ArrayList<CreateResponse>(1); if (entities.getInput().size() == 0) { //Return null return null; } else if (entities.getInput().size() == 1) { //Return a new BatchCreateResult with a null list return new BatchCreateResult<Long, Greeting>(null); } else { //Return a new BatchCreateResult with a response list that has a null inside of it responses.add(new CreateResponse(1l)); responses.add(null); return new BatchCreateResult<Long, Greeting>(responses); } } @RestMethod.BatchUpdate public BatchUpdateResult<Long, Greeting> batchUpdate(BatchUpdateRequest<Long, Greeting> entities) { final Map<Long, UpdateResponse> responseMap = new HashMap<Long, UpdateResponse>(); responseMap.put(3l, new UpdateResponse(HttpStatus.S_201_CREATED)); final Map<Long, RestLiServiceException> errorsMap = new HashMap<Long, RestLiServiceException>(); errorsMap.put(8l, new RestLiServiceException(HttpStatus.S_202_ACCEPTED)); if (entities.getData().containsKey(1l)) { //Return a null BatchUpdateResult return null; } else if (entities.getData().containsKey(2l)) { //Return a BatchUpdateResult with a null results Map return new BatchUpdateResult<Long, Greeting>(null); } else if (entities.getData().containsKey(3l)) { //Return a BatchUpdateResult with a null errors Map return new BatchUpdateResult<Long, Greeting>(responseMap, null); } else if (entities.getData().containsKey(4l)) { //Return a BatchUpdateResult with a errors Map that has a null key in it errorsMap.put(null, new RestLiServiceException(HttpStatus.S_202_ACCEPTED)); return new BatchUpdateResult<Long, Greeting>(responseMap, errorsMap); } else if (entities.getData().containsKey(5l)) { //Return a BatchUpdateResult with a errors Map that has a null value in it errorsMap.put(9l, null); return new BatchUpdateResult<Long, Greeting>(responseMap, errorsMap); } else if (entities.getData().containsKey(6l)) { //Return a BatchUpdateResult with a map that has a null key in it responseMap.put(null, new UpdateResponse(HttpStatus.S_201_CREATED)); return new BatchUpdateResult<Long, Greeting>(responseMap); } else { /* * Return a BatchUpdateResult with java.util.concurrent.ConcurrentHashMap(s). * This test is in place because certain map implementations, such as ConcurrentHashMap, can throw an NPE when * calling contains(null). We want to verify that checking for the existence of nulls in maps returned by * Rest.li resource methods do not cause such NPEs. * This is one of the few cases in this file where an error will not be generated by Rest.li. */ final Map<Long, UpdateResponse> concurrentResponseMap = new ConcurrentHashMap<Long, UpdateResponse>(responseMap); return new BatchUpdateResult<Long, Greeting>(concurrentResponseMap, new ConcurrentHashMap<Long, RestLiServiceException>()); } } @RestMethod.BatchPartialUpdate public BatchUpdateResult<Long, Greeting> batchUpdate(BatchPatchRequest<Long, Greeting> entityUpdates) { final Map<Long, UpdateResponse> responseMap = new HashMap<Long, UpdateResponse>(); responseMap.put(3l, new UpdateResponse(HttpStatus.S_201_CREATED)); if (entityUpdates.getData().containsKey(1l)) { //Return a null BatchUpdateResult return null; } else if (entityUpdates.getData().containsKey(2l)) { //Return a BatchUpdateResult with a null results Map return new BatchUpdateResult<Long, Greeting>(null); } else if (entityUpdates.getData().containsKey(3l)) { //Return a BatchUpdateResult with a null errors Map return new BatchUpdateResult<Long, Greeting>(responseMap, null); } else { //Return a BatchUpdateResult with a map that has a null key in it responseMap.put(null, new UpdateResponse(HttpStatus.S_201_CREATED)); return new BatchUpdateResult<Long, Greeting>(responseMap); } } @RestMethod.BatchDelete public BatchUpdateResult<Long, Greeting> batchDelete(BatchDeleteRequest<Long, Greeting> deleteRequest) { return null; } @RestMethod.Delete public UpdateResponse delete(Long key) { return null; } @Action(name = "returnNullStringArray") public StringArray returnNullStringArray() { return null; } @Action(name = "returnStringArrayWithNullElement") public StringArray returnStringArrayWithNullElement() { final List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add(null); stringList.add("def"); //Return a StringArray with a null element return new StringArray(stringList); } @Action(name = "returnNullActionResult") public ActionResult<Integer> returnNull() { return null; } @Action(name = "returnActionResultWithNullValue") public ActionResult<Integer> returnActionResultWithNullValue() { //Return an ActionResult with a null Value final Integer nullInteger = null; return new ActionResult<Integer>(nullInteger); } @Action(name = "returnActionResultWithNullStatus") public ActionResult<Integer> returnActionResultWithNullStatus() { //Return an ActionResult with a null HttpStatus return new ActionResult<Integer>(3, null); } @Finder("finderCallbackNullList") public void finderCallbackNull(@PagingContextParam final PagingContext a, @QueryParam("tone") final Tone b, @CallbackParam final Callback<List<Greeting>> callback) { final Runnable requestHandler = new Runnable() { public void run() { try { //Depending on the tone, we return a null list or a list with a null element callback.onSuccess(searchReturnNullList(a, b)); } catch (final Throwable throwable) { callback.onError(throwable); } } }; SCHEDULER.schedule(requestHandler, DELAY, TimeUnit.MILLISECONDS); } @Finder("finderPromiseNullList") public Promise<List<Greeting>> finderPromiseNullList(@PagingContextParam final PagingContext a, @QueryParam("tone") final Tone b) { final SettablePromise<List<Greeting>> result = Promises.settable(); final Runnable requestHandler = new Runnable() { public void run() { try { //Depending on the tone, we return a null list or a list with a null element result.done(searchReturnNullList(a, b)); } catch (final Throwable throwable) { result.fail(throwable); } } }; SCHEDULER.schedule(requestHandler, DELAY, TimeUnit.MILLISECONDS); return result; } @Finder("finderTaskNullList") public Task<List<Greeting>> finderTaskNullList(@PagingContextParam final PagingContext a, @QueryParam("tone") final Tone b) { return new BaseTask<List<Greeting>>() { protected Promise<List<Greeting>> run(final com.linkedin.parseq.Context context) throws Exception { //Depending on the tone, we return a null list or a list with a null element return Promises.value(searchReturnNullList(a, b)); } }; } }