/*
Copyright (c) 2012 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.example.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import com.linkedin.data.transform.DataProcessingException;
import com.linkedin.restli.common.HttpStatus;
import com.linkedin.restli.common.PatchRequest;
import com.linkedin.restli.example.Photo;
import com.linkedin.restli.example.PhotoFormats;
import com.linkedin.restli.server.BatchResult;
import com.linkedin.restli.server.CreateResponse;
import com.linkedin.restli.server.PagingContext;
import com.linkedin.restli.server.ResourceLevel;
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.Finder;
import com.linkedin.restli.server.annotations.Optional;
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.resources.CollectionResourceTemplate;
import com.linkedin.restli.server.util.PatchApplier;
/**
* @author kjin
*/
// declare the class to handle collection resources
// collection name "photos", can be any name appropriate
// namespace is used in the generated IDL file, which will be used in the client
// namespace must matches the namespace of the schema
@RestLiCollection(name = "photos", namespace = "com.linkedin.restli.example.photos")
// first template type as the type of the key of the collection resource
// second as the type of the record template itself
public class PhotoResource extends CollectionResourceTemplate<Long, Photo>
{
public PhotoDatabase getDb()
{
return _db;
}
// basic overridable functions for resource template
// corresponding builder class are generated to src/mainGeneratedRest/java/<namespace>
@Override
public CreateResponse create(Photo entity)
{
final Long newId = _db.getCurrentId();
//ID and URN are required fields, so use a dummy value to denote "empty" fields
if ((entity.hasId() && entity.getId() != -1) || (entity.hasUrn() && !entity.getUrn().equals("")))
{
throw new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST,
"Photo ID is not acceptable in request");
}
// overwrite ID and URN in entity
entity.setId(newId);
entity.setUrn(String.valueOf(newId));
_db.getData().put(newId, entity);
return new CreateResponse(newId);
}
// return stored photo
// if the key does not exist, return null, and rest.li will respond HTTP 404 to client
@Override
public Photo get(Long key)
{
return _db.getData().get(key);
}
@Override
public BatchResult<Long, Photo> batchGet(Set<Long> ids)
{
Map<Long, Photo> result = new HashMap<Long, Photo>();
Map<Long, RestLiServiceException> errors =
new HashMap<Long, RestLiServiceException>();
for (Long key : ids)
{
if (get(key) != null)
{
result.put(key, get(key));
}
else
{
errors.put(key, new RestLiServiceException(HttpStatus.S_404_NOT_FOUND,
"No photo with id=" + key
+ " has been found."));
}
}
return new BatchResult<Long, Photo>(result, errors);
}
// update an existing photo with given entity
@Override
public UpdateResponse update(Long key, Photo entity)
{
final Photo currPhoto = _db.getData().get(key);
if (currPhoto == null)
{
return new UpdateResponse(HttpStatus.S_404_NOT_FOUND);
}
//Disallow changing entity ID and URN
//ID and URN are required fields, so use a dummy value to denote "empty" fields
if ((entity.hasId() && entity.getId() != -1) || (entity.hasUrn() && !entity.getUrn().equals("")))
{
throw new RestLiServiceException(HttpStatus.S_400_BAD_REQUEST,
"Photo ID is not acceptable in request");
}
// make sure the ID in the entity is consistent with the key in the database
entity.setId(key);
entity.setUrn(String.valueOf(key));
_db.getData().put(key, entity);
return new UpdateResponse(HttpStatus.S_204_NO_CONTENT);
}
// delete an existing photo
@Override
public UpdateResponse delete(Long key)
{
final boolean isRemoved = (_db.getData().remove(key) != null);
// Remove this photo from all albums to maintain referential integrity.
AlbumEntryResource.purge(_entryDb, null, key);
return new UpdateResponse(isRemoved ? HttpStatus.S_204_NO_CONTENT
: HttpStatus.S_404_NOT_FOUND);
}
// allow partial update to an existing photo
@Override
public UpdateResponse update(Long key, PatchRequest<Photo> patchRequest)
{
final Photo p = _db.getData().get(key);
if (p == null)
{
return new UpdateResponse(HttpStatus.S_404_NOT_FOUND);
}
try
{
PatchApplier.applyPatch(p, patchRequest);
}
catch (DataProcessingException e)
{
return new UpdateResponse(HttpStatus.S_400_BAD_REQUEST);
}
// photo's id and URN should not be changed
p.setId(key);
p.setUrn(String.valueOf(key));
_db.getData().put(key, p);
return new UpdateResponse(HttpStatus.S_202_ACCEPTED);
}
// find photos by title and/or format
// if both title and format are empty, any photo meets the search criteria
@Finder("titleAndOrFormat")
public List<Photo> find(@PagingContextParam PagingContext pagingContext,
@QueryParam("title") @Optional String title,
@QueryParam("format") @Optional PhotoFormats format)
{
final List<Photo> photos = new ArrayList<Photo>();
int index = 0;
final int begin = pagingContext.getStart();
final int end = begin + pagingContext.getCount();
final Collection<Photo> dbPhotos = _db.getData().values();
for (Photo p : dbPhotos)
{
if (index == end)
{
break;
}
else if (index >= begin)
{
if (title == null || p.getTitle().equalsIgnoreCase(title))
{
if (format == null || format == p.getFormat())
{
photos.add(p);
}
}
}
index++;
}
return photos;
}
// custom action defined on collection level without any parameter
// call with "http://<hostname>:<port>/photos?action=purge"
// return JSON object of the action result
// if called on wrong resource level, HTTP 400 is responded
@Action(name = "purge", resourceLevel = ResourceLevel.COLLECTION)
public int purge()
{
final int numPurged = _db.getData().size();
_db.getData().clear();
AlbumEntryResource.purge(_entryDb, null, null);
return numPurged;
}
public final static String URN_ENTITY_TYPE = "photo";
// use dependency injection instead of hard-coding database instance
// photo-server-cmpt will define a bean with the same name as declared here ("photoDb"),
// which points to an implementation of the PhotoDatabase interface
@Inject
@Named("photoDb")
private PhotoDatabase _db;
// need this to cascade deletes
@Inject
@Named("albumEntryDb")
private AlbumEntryDatabase _entryDb;
}