Como eles estão sempre por perto para descarregarmos bytes neles, é fácil aceitar as unidades de disco e os sistemas de arquivos neles como uma coisa natural.
Quando você está escrevendo um arquivo, não precisa se preocupar com muito mais além de sua localização, permissões e requisitos de espaço. Basta construir um java.io.File e começar a trabalhar.
O java.io.File funciona do mesmo jeito, seja em um computador desktop, servidor da Web ou dispositivo remoto. Mas quando você começa a trabalhar com o Google App Engine (GAE), essa transparência, ou a falta dela, torna-se aparente muito rápido.
No GAE, não é possível gravar arquivos em disco, já que não há sistema de arquivos utilizável. Na verdade, somente declarar um java.io.FileInputStream geraria um erro de compilação, porque essa classe foi incluída na lista de bloqueio a partir do GAE SDK.
Felizmente, a vida oferece opções, e o GAE oferece algumas opções particularmente eficientes para armazenamento. Como ele foi elaborado a partir do zero tendo a escalabilidade como objetivo, o GAE oferece dois armazenamentos de valor da chave: o Datastore (também conhecido como Bigtable) retém os dados normais que você normalmente jogaria em um banco de dados, enquanto o Blobstore mantém blobs binários enormes. Os dois possuem acesso de tempo constante e são completamente diferentes dos sistemas de arquivos com os quais você já trabalhou.
Além destes dois, há o Google Storage for Developers. Ele trabalha como o Amazon S3, que também é bastante diferente de um sistema de arquivos tradicional.
Neste artigo, construiremos um aplicativo de exemplo que implementa cada opção de armazenamento do GAE, um por um. Você irá adquirir uma experiência prática usando Bigtable, Blobstore e Google Storage for Developers, e entender os prós e contras de cada implementação.
Você precisará de:
Uma conta do GAE e diversas
ferramentas grátis de software livre para trabalhar nos exemplos deste artigo. Para o seu
ambiente de desenvolvimento, você precisará de JDK 5 ou JDK 6 e Eclipse IDE for
Java developers. Você também precisará de:
Se não conseguir obter acesso ao
Google Storage imediatamente, você pode seguir os exemplos para
Bigtable e Blobstore e obter uma boa noção de como o Google Storage
funciona.
Configuração preliminar: O aplicativo de exemplo
Antes de começarmos a explorar os sistemas de armazenamento do GAE,
precisamos criar as três classes necessárias para nosso aplicativo de
exemplo:
- Um bean que represente uma fotografia. Photo contém campos como título e legenda, além de outros para armazenar dados de imagem binária.
- Um DAO que persista Photos no armazenamento de dados do GAE, também conhecido como Bigtable. O DAO contém um método para inserção de Photos
e outro para extraí-los por meio de ID. Ele usa uma biblioteca de
software livre chamada Objectify-Appengine para persistência. - Um servlet que use o padrão Template Method para
sintetizar um fluxo de trabalho de três etapas. Usaremos o fluxo de
trabalho para explorar cada opção de armazenamento do GAE.
Fluxo de trabalho do aplicativo
Seguiremos o mesmo procedimento para cada uma das opções de
armazenamento de dados do GAE. Isso lhe dará a oportunidade de focar a
tecnologia – e também comparar os prós e os contras de cada método de
armazenamento. O fluxo de trabalho do aplicativo será o mesmo todas as
vezes:
- Exibir e fazer upload do formulário.
- Fazer upload de uma imagem para armazenamento e salvar um registro no armazenamento de dados.
- Apresentar a imagem.
A figura 1 é um diagrama do fluxo de trabalho do aplicativo:
Figura 1. O fluxo de trabalho de três etapas usado para demonstrar cada opção de armazenamento
Como benefício adicional, o aplicativo de exemplo também permite que
você pratique tarefas que são essenciais para qualquer projeto do GAE
que grave e forneça binários. Agora, vamos começar a criar essas
classes!
Um aplicativo simples para GAE
Faça o download do Eclipse se não o tiver e depois instale o plug-in do Google para Eclipse e crie um novo projeto do Google Web Application que não use GWT. Consulte o código de amostra
incluído ao final deste artigo para obter orientação sobre arquivos de
estruturação de projeto.
Assim que tiver configurado seu Web app do
Google, inclua a primeira classe do aplicativo, Photo, como mostrado na listagem 1. Observe que omiti os getters e setters.
Listagem 1. Photo
import javax.persistence.Id;
public class Photo {
@Id
private Long id;
private String title;
private String caption;
private String contentType;
private byte[] photoData;
private String photoPath;
public Photo() {
}
public Photo(String title, String caption) {
this.title = title;
this.caption = caption;
}
// getters and setters omitted
}
A anotação @Id designa qual campo é uma chave primária, o que será importante quando começarmos a trabalhos com o Objectify. Cada registro salvo no armazenamento de dados, também chamado de entidade, requer uma chave primária.
Quando uma imagem é transferida por upload, uma opção é armazená-la diretamente em photoData, que é uma array de bytes. Ela é gravada no armazenamento de dados como propriedadeBlob, juntamente com o restante dos campos de Photo.
Em outras palavras, a imagem é salva e trazida junto ao bean. Se, em vez disso, uma imagem for transferida por upload para o Blobstore ou o Google Storage, os dados são armazenados externamente nesse sistema e photoPath aponta sua localização.
Apenas photoData ou photoPath é usada em qualquer um dos casos. A figura 2 esclarece a função de cada uma:
Figura 2. Como photoData e photoPath funcionam
A seguir, abordaremos a persistência do bean.
Persistência baseada em objeto
Como mencionado anteriormente, usaremos Objectify para criar uma DAO para o bean de Photo. Embora JDO e JPA possam ser APIs de persistência
mais populares e comuns, elas possuem curvas de aprendizado mais acentuadas.
Outra opção seria usar
a API de armazenamento de dados de baixo nível do GAE, mas isso envolve o tedioso trabalho de serializar
os beans para e de entidades de armazenamento de dados. O Objectify toma conta disso, por meio de
reflexo Java. (Ao final do artigo consulte Recursos para saber mais sobre alternativas de persistência do GAE, incluindo Objectify-Appengine.)
Comece criando uma classe chamada PhotoDao e codificando-a
como mostrado na listagem 2:
Listagem 2. PhotoDao
import com.googlecode.objectify.*;
import com.googlecode.objectify.helper.DAOBase;
public class PhotoDao extends DAOBase {
static {
ObjectifyService.register(Photo.class);
}
public Photo save(Photo photo) {
ofy().put(photo);
return photo;
}
public Photo findById(Long id) {
Key<Photo> key = new Key<Photo>(Photo.class, id);
return ofy().get(key);
}
}
PhotoDao estende DAOBase, uma classe de conveniência que carrega lentamente uma instância Objectify.
Objectify é nossa interface primária na API e é exposto através do método ofy. Antes de usarmos ofy, no entanto, precisamos registrar classes de persistência em um inicializador estático como Photo na listagem 2.
O DAO contém dois métodos para inserção e descoberta de Photos. Em cada um deles, trabalhar com o Objectify é tão simples quanto trabalhar com hashtable. Você deve notar que Photos são trazidas com um Key em findById, mas não se preocupe com isso: para os fins deste artigo, pense em Key como um wrapper em torno do campo id.
Agora temos um bean de Photo e uma PhotoDao para gerenciar a persistência. A seguir, detalharemos o fluxo de trabalho do aplicativo.
Fluxo de trabalho do aplicativo, por meio do padrão Template Method
Se você já jogou Mad Libs, o padrão Template Method fará bastante
sentido para você. Cada Mad Lib apresenta uma história com muitos
trechos em branco para que os leitores os preencham. A entrada do leitor – como os trechos em branco são completados – altera a história
drasticamente. De modo semelhante, as classes que usam o padrão Template
Method contêm uma série de etapas, e algumas são deixadas em branco.
Construiremos um servlet que use o padrão Template Method para realizar o fluxo de trabalho
do nosso aplicativo de exemplo. Comece separando um servlet abstrato, nomeando-o AbstractUploadServlet. É possível usar o código na listagem 3 como referência:
Listagem 3. AbstractUploadServlet
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
@SuppressWarnings("serial")
public abstract class AbstractUploadServlet extends HttpServlet {
}
Em seguida, adicione os três métodos abstratos da listagem 4. Cada um representa uma etapa do fluxo de trabalho.
Listagem 4. Três métodos abstratos
protected abstract void showForm(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException;
protected abstract void handleSubmit(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException;
protected abstract void showRecord(long id, HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException;
Agora, já que estamos usando o padrão Template Method, pense nos métodos da listagem 4 como os trechos em branco, e no código da listagem 5 como a história
que os une:
Listagem 5. O surgimento de um fluxo de trabalho
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getParameter("action");
if ("display".equals(action)) {
// don't know why GAE appends underscores to the query string
long id = Long.parseLong(req.getParameter("id").replace("_", ""));
showRecord(id, req, resp);
} else {
showForm(req, resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
handleSubmit(req, resp);
}
Um lembrete sobre servlets
Caso já faça algum tempo desde que você trabalhou pela última vez com servlets antigos simples, doGet e doPost são métodos padrão para manipular HTTP GETs e POSTs. É uma prática comum usar GET para buscar recurso da Web e POST para enviar dados.
Sendo assim, nossa implementação de doGet exibe um formulário de upload ou uma foto do armazenamento e doPost trata dos envios dos formulários para upload. Fica a cargo das classes que estendem AbstractUploadServlet definirem cada parte do comportamento.
O diagrama na figura 3 mostra a sequência de eventos ocorridos. Pode levar alguns minutos para se ter uma visão clara do que está realmente acontecendo.
Figura 3. O fluxo de trabalho em um diagrama de sequência
Com as três classes construídas, nosso aplicativo de exemplo está pronto
para funcionar. Agora podemos focar como cada uma das opções de
armazenamento do GAE interage com o fluxo de trabalho do aplicativo,
começando com o Bigtable.
Opção nº 1 de armazenamento do GAE: Bigtable
A documentação do GAE do Google descreve o Bigtable como uma array
fragmentada e classificada, mas acho
mais fácil pensar nele como uma hashtable gigante repartida entre
servidores enormes.
Como um banco de dados relacional, o Bigtable possui tipos de dados.
Na verdade, tanto o Bigtable quanto os bancos de dados relacionais usam
o tipo blob para armazenar binários.
Trabalhar com blobs no Bigtable é mais prático porque eles estão
carregados ao lado de
outros campos, tornando-o disponíveis imediatamente. A única
limitação é que os blobs
não podem ser maiores que 1 MB, embora essa restrição deva ser
relaxada no futuro.
Seria bastante difícil encontrar uma câmera digital hoje em dia que
tire fotos menores
do que isso, de modo que usar o Bigtable pode representar um
retrocesso para qualquer caso de uso que envolva
imagens (como o nosso aplicativo de exemplo).
Se a regra de 1 MB
estiver bem para você, ou se você vai armazenar algo menor do que
imagens, o Bigtable pode ser uma boa escolha – das três alternativas de
armazenamento do GAE, ela é a mais fácil de se trabalhar.
Antes de fazer o upload de dados para o Bigtable, precisaremos criar
um formulário de upload. Em seguida,
trabalharemos na implementação de servlet, que consiste em três
métodos abstratos
customizados para o Bigtable. Finalmente, implementaremos a
manipulação de erros, já que o limite de 1 MB é fácil de ser quebrado.
Observação: Não confunda o tipo blob com o Blobstore – que é outro armazenamento de valor da chave do GAE, e que exploraremos a seguir.
Crie o formulário de upload
A figura 4 mostra o formulário de upload do Bigtable:
Figura 4. Um formulário de upload do Bigtable
Para criar este formulário, comece com um arquivo chamado datastore.jsp e depois insira o bloco de
código da listagem 6:
Listagem 6. O formulário de upload customizado
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form method="POST" enctype="multipart/form-data">
<table>
<tr>
<td>Title</td>
<td><input type="text" name="title" /></td>
</tr>
<tr>
<td>Caption</td>
<td><input type="text" name="caption" /></td>
</tr>
<tr>
<td>Upload</td>
<td><input type="file" name="file" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit" /></td>
</tr>
</table>
</form>
</body>
</html>
O formulário deve ter seu atributo de método definido como POST e um tipo incluído de dados de formulário/multipartes. Como nenhum atributo de action
é especificado, o formulário é enviado para si mesmo. Usando POST, terminamos no AbstractUploadServlet’s doPost, que por sua vez chama handleSubmit.
Temos o formulário, então vamos prosseguir com o servlet por trás dele.
Upload para e do Bigtable
Aqui, implementamos os três métodos, um por vez. Um exibe o
formulário que acabamos de criar e outro processa seus uploads. O último
método nos fornece os uploads de volta, assim você pode ver como isso é
feito.
O servlet usa a biblioteca Apache Commons
FileUpload. Faça o download da biblioteca e de suas dependências e inclua-as em seu
projeto. Quando isso estiver feito, modele o stub na listagem 7:
Listagem 7. DatastoreUploadServlet
import info.johnwheeler.gaestorage.core.*;
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
@SuppressWarnings("serial")
public class DatastoreUploadServlet extends AbstractUploadServlet {
private PhotoDao dao = new PhotoDao();
}
Não há nada muito interessante acontecendo aqui. Importamos as classes das quais precisamos e construímos uma PhotoDao para usar mais tarde. DatastoreUploadServlet não fará nenhuma compilação até que implementemos os métodos abstratos. Vamos ver cada um deles, começando com showForm na listagem 8:
Listagem 8. showForm
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getRequestDispatcher("datastore.jsp").forward(req, resp);
}
Como se pode ver, showForm simplesmente encaminha para o nosso formulário de upload. handleSubmit, mostrado na listagem 9, está mais envolvido:
Listagem 9. handleSubmit
@Override
protected void handleSubmit(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
ServletFileUpload upload = new ServletFileUpload();
try {
FileItemIterator it = upload.getItemIterator(req);
Photo photo = new Photo();
while (it.hasNext()) {
FileItemStream item = it.next();
String fieldName = item.getFieldName();
InputStream fieldValue = item.openStream();
if ("title".equals(fieldName)) {
photo.setTitle(Streams.asString(fieldValue));
continue;
}
if ("caption".equals(fieldName)) {
photo.setCaption(Streams.asString(fieldValue));
continue;
}
if ("file".equals(fieldName)) {
photo.setContentType(item.getContentType());
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.copy(fieldValue, out, true);
photo.setPhotoData(out.toByteArray());
continue;
}
}
dao.save(photo);
resp.sendRedirect("datastore?action=display&id=" + photo.getId());
} catch (FileUploadException e) {
throw new ServletException(e);
}
}
É uma longa linha de código, mas o que ela faz é simples. O método handleSubmit transmite o corpo da solicitação do formulário de upload, extraindo cada valor do formulário em um FileItemStream.
Enquanto isso, um Photo é configurado parte por parte. É um pouco chato rolar por cada campo e verificar o que é o quê, mas é assim que se faz com dados de fluxo e com a API de fluxo.
Voltando ao código, quando chegamos ao campo de arquivo, um ByteArrayOutputStream ajuda a repartir os bytes transferidos por download em photoData. Por último, salvamos Photo com PhotoDao e enviamos um redirecionamento, que nos leva à nossa classe abstrata final, showRecord na listagem 10:
Listagem 10. showRecord
@Override
protected void showRecord(long id, HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
Photo photo = dao.findById(id);
resp.setContentType(photo.getContentType());
resp.getOutputStream().write(photo.getPhotoData());
resp.flushBuffer();
}
showRecord olha um Photo e define um cabeçalho com o tipo de conteúdo antes de gravar a array de bytes photoData diretamente na resposta HTTP. flushBuffer empurra qualquer conteúdo restante para o navegador.
A última coisa que precisamos fazer é adicionar código de manipulação de erros para uploads maiores que 1 MB.
Exibição de uma mensagem de erro
Como mencionado anteriormente, o Bigtable impõe um limite de 1 MB, que é
um desafio não
ultrapassar quando a maioria dos casos de uso envolve imagens. O
máximo que podemos fazer é dizer aos usuários para redimensionarem
suas imagens e tentarem novamente.
Para fins de demonstração, o
código na listagem 11 simplesmente exibe uma mensagem de exceção quando
uma exceção do GAE é acionada. (Observe que isso é uma manipulação de
erros específica para servlet padrão e não específica para GAE.)
Listagem 11. Ocorreu um erro
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
@SuppressWarnings("serial")
public class ErrorServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
String message = (String)
req.getAttribute("javax.servlet.error.message");
PrintWriter out = res.getWriter();
out.write("<html>");
out.write("<body>");
out.write("<h1>An error has occurred</h1>");
out.write("<br />" + message);
out.write("</body>");
out.write("</html>");
}
}
Não se esqueça de registrar ErrorServlet em web.xml, junto com os outros servlets que criaremos no decorrer deste artigo. O código na listagem 12 registra uma página de erro que aponta de volta para ErrorServlet:
Listagem 12. Registro do erro
<servlet>
<servlet-name>errorServlet</servlet-name>
<servlet-class>
info.johnwheeler.gaestorage.servlet.ErrorServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>errorServlet</servlet-name>
<url-pattern>/error</url-pattern>
</servlet-mapping>
<error-page>
<error-code>500</error-code>
<location>/error</location>
</error-page>
Isso encerra nossa rápida introdução ao Bigtable, também conhecido como o armazenamento de dados do GAE.
O Bigtable é provavelmente a mais intuitiva das opções de armazenamento do GAE, mas sua desvantagem
é o tamanho do arquivo: com 1 MB por arquivo, provavelmente você não irá querer usá-lo para nada maior
do que uma miniatura – se tanto.
Em seguida vem o Blobstore, uma outra opção de armazenamento de valor da chave
que pode salvar e fornecer arquivos de até 2 GB de tamanho.
Opção nº 2 de armazenamento do GAE: Blobstore
O Blobstore possui a vantagem do tamanho em relação ao Bigtable, mas
também seus próprios problemas. O fato de que ele força o uso de uma URL única de upload, o
que torna difícil construir serviços da Web em torno dela. Aí vai um
exemplo de como ela se parece:
/_ah/upload/aglub19hcHBfaWRyGwsSFV9fQmxvYlVwbG9hZFNlc3Npb25fXxh9DA
Os clientes de serviços da Web devem pedir a URL antes de usar o POST nela,
o que resulta em uma chamada extra pelo fio. Isso não deve ser um
grande problema em muitos aplicativos, mas não é muito elegante.
Também
poderia ser proibido em casos em que o cliente está executando em GAE –
em que as horas de CPU são cobradas. Se você estiver pensando em
resolver essas questões construindo um servlet que encaminhe os uploads
para a URL única através de URLFetch, pense novamente.
O URLFetch possui
uma restrição de transferência de 1 MB, de modo que você pode usar o
Bigtable também se estiver indo nessa direção. Para quadro de
referência, o gráfico na figura 5 mostra a diferença entre uma chamada
de serviço da Web de uma e de duas vias:
Figura 5. A diferença entre uma chamada de serviço da Web de uma e de duas vias
O Blobstore possui seus prós e contras, e você verá mais a respeito nas
próximas seções. Mais uma vez, iremos construir um formulário de upload e
implementar os três métodos abstratos fornecidos por AbstractUploadServlet, mas desta vez, iremos padronizar nosso código para Blobstore.
Um formulário de upload do Blobstore
Não há muito o que adaptar em nosso formulário de upload do Blobstore. Basta copiar datastore.jsp
para um arquivo chamado blobstore.jsp e depois aumentá-lo com as linhas de código em negrito mostradas na listagem 13:
Listagem 13. blobstore.jsp
<% String uploadUrl = (String) request.getAttribute("uploadUrl"); %><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form method="POST" action="<%= uploadUrl %>"
enctype="multipart/form-data">
<!-- labels and fields omitted -->
</form>
</body>
</html>
A URL única de upload é gerada em um servlet, que iremos codificar a
seguir. Aqui, essa URL é analisada fora da solicitação e colocada no
atributo de ação do formulário.
Não temos nenhum controle sobre o
servlet do Blobstore para o qual estamos fazendo o upload, então como
vamos obter os outros valores do formulário? A resposta é que a API do
Blobstore possui um mecanismo de retorno de chamada.
Passamos uma URL de
retorno de chamada para a API quando a URL única é gerada. Depois do
upload, o Blobstore chama o retorno de chamada, passando a solicitação
original junto com qualquer blob transferido por upload. Você verá tudo
isso em ação quando implementarmos AbstractUploadServlet a seguir.
Upload para o Blobstore
Comece usando a listagem 14 como referência para separar uma classe chamada BlobstoreUploadServlet, que estende AbstractUploadServlet:
Listagem 14. BlobstoreUploadServlet
import info.johnwheeler.gaestorage.core.*;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import com.google.appengine.api.blobstore.*;
@SuppressWarnings("serial")
public class BlobstoreUploadServlet extends AbstractUploadServlet {
private BlobstoreService blobService =
BlobstoreServiceFactory.getBlobstoreService();
private PhotoDao dao = new PhotoDao();
}
A definição de classe inicial é semelhante ao que fizemos com DatastoreUploadServlet, com a inclusão de uma variável BlobstoreService. É isso que gera URL única em showForm na listagem 15:
Listagem 15. showForm para Blobstore
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String uploadUrl = blobService.createUploadUrl("/blobstore");
req.setAttribute("uploadUrl", uploadUrl);
req.getRequestDispatcher("blobstore.jsp").forward(req, resp);
}
O código na listagem 15 cria uma URL de upload e a configura na solicitação. O código encaminha para o formulário criado na listagem 13, no qual a URL de upload é esperada.
A URL de retorno de chamada é configurada para esse contexto do servlet, conforme definido em web.xml. Assim, quando os POSTs voltam, acabamos em handleSubmit mostrada na listagem 16:
Listagem 16. handleSubmit para Blobstore
@Override
protected void handleSubmit(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
Map<String, BlobKey> blobs = blobService.getUploadedBlobs(req);
BlobKey blobKey = blobs.get(blobs.keySet().iterator().next());
String photoPath = blobKey.getKeyString();
String title = req.getParameter("title");
String caption = req.getParameter("caption");
Photo photo = new Photo(title, caption);
photo.setPhotoPath(photoPath);
dao.save(photo);
resp.sendRedirect("blobstore?action=display&id=" + photo.getId());
}
getUploadedBlobs retorna um Map do BlobKeys. Como o nosso formulário de upload suporta um único upload, pegamos o único BlobKey esperado e colocamos uma representação de sequência dele na variável photoPath.
Em seguida, o restante dos campos é analisado em variáveis e configurado em uma nova instância Photo. A instância é salva no armazenamento de dados antes de redirecionar para showRecord na listagem 17:
Listagem 17. showRecord para Blobstore
@Override
protected void showRecord(long id, HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
Photo photo = dao.findById(id);
String photoPath = photo.getPhotoPath();
blobService.serve(new BlobKey(photoPath), resp);
}
Em showRecord, o Photo que acabamos de salvar em handleSubmit é recarregado a partir do Blobstore. Os bytes reais de qualquer coisa que tenha sido transferida por upload não são armazenados no bean, como eram no Bigtable. Em vez disso, um BlobKey é reconstruído com photoPath e usado para fornecer uma imagem para o navegador.
O Blobstore torna o trabalho com uploads baseados em formulário muito fácil, mas os uploads baseados em serviço da Web são uma outra coisa. A seguir veremos o Google Storage for Developers, que nos traz a questão exatamente oposta: os uploads baseados em formulários requerem um pouco de hacking, enquanto os uploads baseados em serviço são fáceis.
Opção nº 3 de armazenamento do GAE: Google Storage
O Google Storage for Developers é a mais poderosa das três opções de
armazenamento do GAE,
e é fácil de usar depois de ter tirado algumas dúvidas do caminho.
O
Google Storage tem muito em comum com o Amazon S3. Na verdade, os dois
usam o mesmo protocolo e possuem a mesma interface RESTful, de modo que
as bibliotecas feitas para se trabalhar com o S3, como a JetS3t, também
funcionam no Google Storage.
Infelizmente, no momento em que estou
escrevendo este artigo, essas bibliotecas não funcionam de modo
confiável no Google App Engine porque elas realizam operações não
permitidas como encadeamentos de spawn. Assim, no momento, temos de
trabalhar com a interface RESTful e fazer um pouco do trabalho pesado
que essas APIs poderiam fazer.
Mas vale a pena o trabalho do Google Storage, principalmente porque
ele suporta controles de acesso poderosos através de listas de controle
de acesso (ACLs).
Com as ACLs, é possível conceder acesso somente
leitura e leitura/gravação para objetos, assim você pode tornar as fotos
públicas ou privadas facilmente, como no Facebook e no Flickr. A
s ACLs
estão fora do escopo deste artigo, assim, todo upload será considerado
de acesso público, somente leitura. Consulte a documentação on-line do
Google Storage em Recursos para saber mais sobre as ACLs.
Diferentemente do Blobstore, o Google Storage é compatível por padrão
para uso por serviço da Web e clientes de navegador. Os dados são
enviados através de PUT ou POST. de RESTful.
A
primeira opção é para clientes de serviço da Web que podem controlar
como as solicitações são estruturadas e como os cabeçalhos são gravados.
A segunda opção, que exploraremos aqui, é para uploads baseados em
navegador. Precisaremos de um hack JavaScript para processar o
formulário de upload, que apresenta algumas complicações, como iremos
ver.
Hack do formulário de upload do Google Storage
Diferentemente do Blobstore, o Google Storage não encaminha uma URL de retorno de chamada depois de o
POST ter sido usado nela. Em vez disso, ele emite um
redirecionamento para a URL que especificarmos.
Isso apresenta um
problema, já que os valores dos formulários não são transportados no
redirecionamento. A forma de lidar com isso é criar dois formulários na
mesma página da Web – um contendo os campos de texto de título e
legenda, o outro com o campo de upload de arquivo e os parâmetros
exigidos pelo Google Storage.
Usaremos então o Ajax para enviar o
primeiro formulário. Quando o retorno de chamada do Ajax for chamado,
enviaremos o segundo formulário de upload.
Como esse formulário é mais complicado do que os dois últimos, iremos construí-lo
passo a passo. Primeiro, extraímos alguns valores que são definidos por um servlet de encaminhamento que
ainda não foi construído, mostrado na listagem 18:
Listagem 18. Extração de valores do formulário
<%
String uploadUrl = (String) request.getAttribute("uploadUrl");
String key = (String) request.getAttribute("key");
String successActionRedirect = (String)
request.getAttribute("successActionRedirect");
String accessId = (String) request.getAttribute("GoogleAccessId");
String policy = (String) request.getAttribute("policy");
String signature = (String) request.getAttribute("signature");
String acl = (String) request.getAttribute("acl");
%>
A uploadUrl contém o terminal REST do Google Storage. A
API
fornece os dois mostrados abaixo. Qualquer um é aceitável, mas somos
responsáveis por substituir os componentes em itálico por nossos
próprios valores:
- bucket.commondatastorage.googleapis.com/object
- commondatastorage.googleapis.com/bucket/object
As variáveis restantes são parâmetros exigidos do Google Storage:
- key: O nome dos dados transferidos por upload no Google Storage.
- success_action_redirect: Para onde redirecionar quando o
upload estiver concluído. - GoogleAccessId: Uma chave de API atribuída ao Google.
- policy: Uma cadeia de caractere de base JSON com 64 caracteres codificados, limitando como os dados são transferidos por upload.
- signature: A política assinalada com um algoritmo hash e uma base de 64 caracteres codificados. Usada para autenticação.
- acl: Uma especificação da lista de controle de acesso.
Dois formulários e um botão de enviar
O primeiro formulário na listagem 19 contém somente os campos de título e legenda. As tags do <html> circundante e do <body> foram omitidas.
Listagem 19. O primeiro formulário de upload
<form id="fieldsForm" method="POST">
<table>
<tr>
<td>Title</td>
<td><input type="text" name="title" /></td>
</tr>
<tr>
<td>Caption</td>
<td>
<input type="hidden" name="key" value="<%= key %>" />
<input type="text" name="caption" />
</td>
</tr>
</table>
</form>
Não há muito o que dizer sobre esse formulário, exceto que ele usa o POST para postar
para si mesmo. Vamos para o formulário da listagem 20, que é maior porque
contém uma meia dúzia de campos de entrada ocultos:
Listagem 20. O segundo formulário com campos ocultos
<form id="uploadForm" method="POST" action="<%= uploadUrl %>"
enctype="multipart/form-data">
<table>
<tr>
<td>Upload</td>
<td>
<input type="hidden" name="key" value="<%= key %>" />
<input type="hidden" name="GoogleAccessId"
value="<%= accessId %>" />
<input type="hidden" name="policy"
value="<%= policy %>" />
<input type="hidden" name="acl" value="<%= acl %>" />
<input type="hidden" id="success_action_redirect"
name="success_action_redirect"
value="<%= successActionRedirect %>" />
<input type="hidden" name="signature"
value="<%= signature %>" />
<input type="file" name="file" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="button" value="Submit" id="button"/>
</td>
</tr>
</table>
</form>
Os valores extraídos no scriptlet JSP (na listagem 18) são
colocados nos campos ocultos. A entrada do arquivo está na parte inferior. O botão de enviar é um
botão antigo simples, que não fará nada até que o ajustemos com JavaScript, como mostrado na
listagem 21:
Listagem 21. Envio do formulário de upload
<script type="text/javascript"
src="https://Ajax.googleapis.com/Ajax/libs/jquery/1.4.3/jquery.min.js">
</script>
<script type="text/javascript">
$(document).ready(function() {
$('#button').click(function() {
var formData = $('#fieldsForm').serialize();
var callback = function(photoId) {
var redir = $('#success_action_redirect').val() +
photoId;
$('#success_action_redirect').val(redir)
$('#uploadForm').submit();
};
$.post("gstorage", formData, callback);
});
});
</script>
O JavaScript na listagem 21 é escrito com JQuery. Mesmo que não tenha usado a biblioteca, o código não deve ser difícil de entender.
A primeira coisa que o código faz é importar a JQuery. Depois, um listener de clique é instalado no botão, de modo que quando o botão é clicado, o primeiro formulário é enviado via Ajax.
A partir daí, chegamos ao método handleSubmit do servlet (que construiremos em breve), no qual um Photo é construído e salvo no armazenamento de dados. Finalmente, a nova Photo ID é retornada para o retorno de chamada e anexada à URL em success_action_redirect antes de o formulário de upload ser enviado.
Dessa forma, quando voltamos do redirecionamento, podemos olhar Photo e exibir sua imagem. A figura 6 mostra toda a sequência de eventos:
Figura 6. Um diagrama de sequência mostrando o caminho de chamada do JavaScript
Com o formulário sendo cuidado, precisamos de uma classe do utilitário para criar e assinar documentos sobre políticas. Depois podemos ir para a subclasse AbstractUploadServlet.
Criação e assinatura de um documento sobre políticas
Os documentos sobre políticas limitam os uploads. Por exemplo, devemos especificar o tamanho dos uploads ou que tipos de arquivos são aceitáveis, e podemos até impor restrições em nomes de arquivos.
Depósitos públicos não exigem documentos sobre políticas, mas os depósitos privados como o Google Storage exigem. Para colocar as coisas em movimento, elimine uma classe do utilitário chamada GSUtils baseada no código da listagem 22:
Listagem 22. GSUtils
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.google.appengine.repackaged.com.google.common.util.Base64;
private class GSUtils {
}
Dado que as classes de utilitário são normalmente compostas somente
de métodos estáticos, é uma boa ideia privatizar seus construtores
padrão para evitar instanciação. Com a classe separada, podemos voltar
nossa atenção para a criação de documento sobre políticas.
O documento sobre políticas é formatado em JSON, mas o JSON é
bastante simples de modo que não precisamos recorrer a nenhuma
biblioteca especial. Em vez disso, podemos fazer as coisas à mão, com um
simples StringBuilder.
Primeiro, temos que construir uma
data de ISO8601 e definir o documento sobre políticas que será expirado
por ela. Os uploads não terão êxito quando o documento sobre políticas
expirar.
Depois, temos que colocar as restrições sobre as quais falamos
anteriormente, que são chamadas de condições no documento sobre
políticas. Finalmente, o documento é codificado em uma base de 64 e
retornado para o responsável pela chamada.
Inclua o método da listagem 23 em GSUtils:
Listagem 23. Criação de um documento sobre políticas
public static String createPolicyDocument(String acl) {
GregorianCalendar gc = new GregorianCalendar();
gc.add(Calendar.MINUTE, 20);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
String expiration = df.format(gc.getTime());
StringBuilder buf = new StringBuilder();
buf.append("{\"expiration\": \"");
buf.append(expiration);
buf.append("\"");
buf.append(",\"conditions\": [");
buf.append(",{\"acl\": \"");
buf.append(acl);
buf.append("\"}");
buf.append("[\"starts-with\", \"$key\", \"\"]");
buf.append(",[\"starts-with\", \"$success_action_redirect\", \"\"]");
buf.append("]}");
return Base64.encode(buf.toString().replaceAll("\n", "").getBytes());
}
Usamos um GregorianCalendar que foi ajustado para 20 minutos mais tarde para construir a data de expiração. O código é kludgy, que pode ser impresso no console, copiado e executado por meio de uma ferramenta como o JSONLint.
Em seguida, passamos a acl no documento sobre políticas para evitar o hardcoding nele. Tudo o que for variável deve ser passado como argumento do método, como acl.
Finalmente, o documento é codificado em uma base de 64 antes de ser retornado para o responsável pela chamada. Consulte a documentação do Google Storage para obter mais informações sobre o que é permitido no documento sobre políticas.
Autenticação no Google Storage
Os documentos sobre políticas oferecem duas funções. Além de
impingir políticas, eles são a base
das assinaturas que geramos para autenticar os uploads.
Quando nos
inscrevemos no Google Storage, recebemos uma chave secreta que somente
nós e o Google sabemos. Assinamos o documento do nosso lado com a chave
secreta e o Google o assina com a mesma chave. Se as assinaturas
corresponderem, o upload é permitido. A figura 7 oferece uma boa visão
desse ciclo:
Figura 7. Como os uploads são autenticados no Google Storage
Para gerar a assinatura, usamos os pacotes javax.crypto
e java.security que importamos enquanto separávamos
GSUtils. A listagem 24 mostra os métodos:
Listagem 24. Assinatura de um documento sobre políticas
public static String signPolicyDocument(String policyDocument,
String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA1");
byte[] secretBytes = secret.getBytes("UTF8");
SecretKeySpec signingKey =
new SecretKeySpec(secretBytes, "HmacSHA1");
mac.init(signingKey);
byte[] signedSecretBytes =
mac.doFinal(policyDocument.getBytes("UTF8"));
String signature = Base64.encode(signedSecretBytes);
return signature;
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
Um hashing seguro em código Java envolve rigmarole que eu prefiro não discutir
neste artigo. O que importa é que a listagem 24 mostra como isso é feito adequadamente, e que o hash deve ser codificado em uma base de 64 antes de ser retornado.
Com esses pré-requisitos atendidos, voltamos a um território
familiar: implementar os três métodos abstratos para fazer upload e
recuperar arquivos do Google Storage.
Secure Data Connector do Google
Não iremos trabalhar com o Secure Data Connector do Google neste
artigo, mas vale a pena dar uma olhada nele se estiver planejando usar o
Google Storage. O SDC torna mais fácil acessar dados em seus próprios
sistemas, mesmo que estes estejam por trás de um firewall.
Upload para o Google Storage
Comece separando uma classe chamada GStorageUploadServlet baseada no código da listagem 25:
Listagem 25. GStorageUploadServlet
import info.johnwheeler.gaestorage.core.GSUtils;
import info.johnwheeler.gaestorage.core.Photo;
import info.johnwheeler.gaestorage.core.PhotoDao;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@SuppressWarnings("serial")
public class GStorageUploadServlet extends AbstractUploadServlet {
private PhotoDao dao = new PhotoDao();
}
O método showForm, mostrado na listagem 26, configura os
parâmetros que precisamos passar para o Google Storage através do formulário de upload:
Listagem 26. showForm para Google Storage
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String acl = "public-read";
String secret = getServletConfig().getInitParameter("secret");
String accessKey = getServletConfig().getInitParameter("accessKey");
String endpoint = getServletConfig().getInitParameter("endpoint");
String successActionRedirect = getBaseUrl(req) +
"gstorage?action=display&id=";
String key = UUID.randomUUID().toString();
String policy = GSUtils.createPolicyDocument(acl);
String signature = GSUtils.signPolicyDocument(policy, secret);
req.setAttribute("uploadUrl", endpoint);
req.setAttribute("acl", acl);
req.setAttribute("GoogleAccessId", accessKey);
req.setAttribute("key", key);
req.setAttribute("policy", policy);
req.setAttribute("signature", signature);
req.setAttribute("successActionRedirect", successActionRedirect);
req.getRequestDispatcher("gstorage.jsp").forward(req, resp);
}
Observe que o acl é configurado para leitura pública, assim qualquer coisa que for transferida por upload será visualizada por qualquer um. As próximas variáveis secret, accessKeyeendpoint são usadas para obter a permissão e autenticar com o Google Storage.
Eles são extraídos de init-params declarados em web.xml. Consulte o código de amostra para obter detalhes.
Lembre-se de que, diferentemente do Blobstore, que encaminha para uma URL que nos coloca em showRecord, o Google Storage emite um redirecionamento. A URL de redirecionamento é armazenada em successActionRedirect.
successActionRedirect recorre ao método auxiliar na listagem 27 para construir a URL de redirecionamento.
Listagem 27. getBaseUrl()
private static String getBaseUrl(HttpServletRequest req) {
String base = req.getScheme() + "://" + req.getServerName() + ":" +
req.getServerPort() + "/";
return base;
}
O método auxiliar pesquisa a solicitação recebida para construir a URL de base antes de devolver o controle para showForm. Ao retornar, uma chave é criada com um identificador exclusivo universal ou UUID, que é uma String
exclusiva.
Em seguida, a política e a assinatura são geradas com a
classe do utilitário que construímos. Finalmente, configuramos os
atributos de pedido do JSP antes de encaminhar para ele.
A listagem 28 mostra handleSubmit:
Listagem 28. handleSubmit para Google Storage
@Override
protected void handleSubmit(HttpServletRequest req, HttpServletResponse
resp) throws ServletException, IOException {
String endpoint = getServletConfig().getInitParameter("endpoint");
String title = req.getParameter("title");
String caption = req.getParameter("caption");
String key = req.getParameter("key");
Photo photo = new Photo(title, caption);
photo.setPhotoPath(endpoint + key);
dao.save(photo);
PrintWriter out = resp.getWriter();
out.println(Long.toString(photo.getId()));
out.close();
}
Lembre-se de que quando o primeiro formulário é enviado, somos colocados em handleSubmit por um POST. do Ajax. O upload em si não é identificado ali, mas separadamente, em um retorno de chamada do Ajax.
handleSubmit apenas analisa o primeiro formulário, constrói um Photo e o salva no armazenamento de dados. Depois, a ID de Photo é retornada para o retorno de chamada do Ajax, escrevendo-a no corpo de resposta.
No retorno de chamada, o formulário de upload é enviado para o terminal do Google Storage. Quando o
Google Storage processa o upload, ele é configurado para emitir um redirecionamento de volta para showRecord, na listagem 29:
Listagem 29. showRecord para Google Storage
@Override
protected void showRecord(long id, HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
Photo photo = dao.findById(id);
String photoPath = photo.getPhotoPath();
resp.sendRedirect(photoPath);
}
showRecord olha Photo e redireciona para seu photoPath. photoPath aponta para nossa imagem hospedada nos servidores do Google.
Conclusão
Examinamos três opções de armazenamento centradas no Google e
avaliamos seus prós e contras.
O Bigtable é fácil de se trabalhar, mas impõe um limite de tamanho
de arquivo de 1 MB. Os blobs no Blobstore podem ter
até 2 GB por peça, mas a URL única é difícil de se trabalhar nos
serviços da Web.
Finalmente, o Google Storage for Developers é a opção
mais robusta. Pagamos somente pelo armazenamento que usamos e o céu é o
limite quando se trata da quantidade de dados que pode ser armazenada em
um único arquivo.
O Google Storage também é a solução mais complexa
para se trabalhar, porque suas bibliotecas atualmente não suportam o
GAE. O suporte de uploads baseados em navegador também não é a coisa
mais fácil do mundo.
À medida que o Google App Engine se torna uma plataforma de
desenvolvimento mais popular para desenvolvedores de Java,
entender suas várias opções de armazenamento é essencial.
Neste
artigo, vimos exemplos de implementação simples para Bigtable, Blobstore
e Google Storage for Developers. Quer você escolha uma opção de
armazenamento e fique com ela, quer você use cada uma para um caso de
uso diferente, agora você possui as ferramentas necessárias para
armazenar montanhas de dados no GAE.
Download
Descrição | Nome | Tamanho | Método de download |
---|---|---|---|
Sample code for this article | j-gaestorage.zip | 12KB | HTTP |
Recursos
Aprender
- Google App Engine for Java
(Rick Hightower, developerWorks, agosto de 2009): essa série de três partes levou os leitores do developerWorks para
um giro antecipado no GAE. Ler essa série hoje ainda oferece muitos
esclarecimentos e revela o quanto o GAE foi longe em pouco tempo. - GAE coverage in Java development 2.0 (Andrew Glover, developerWorks): mantendo o foco em tecnologias emergentes importantes, a série Java development 2.0
o atualiza em relação à infraestrutura de ferramentas Java em rápida
expansão. Os tópicos recentes relacionados ao GAE incluíram Gaelyk (com Bigtable) e
Objectify-Appengine (também com Bigtable). - Página inicial do Google Storage for Developers:
leia a documentação on-line e inscreva-se para uma edição prévia do
serviço do Google Storage for Developers, atualmente disponível somente
para um número limitado de desenvolvedores nos Estados Unidos. - Google Storage for Developers no Google I/O 2010:
os engenheiros Mike Schwartz e David Erb conduzem essa introdução de
alto nível, com duração de uma hora, ao Google Storage for Developers. - Navegue pela página da livraria de tecnologia Java para obter livros sobre estes e outros tópicos técnicos.
- Zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
- Página inicial do Google App Engine: saiba mais sobre a infraestrutura de nuvem do Google e inscreva-se para ter uma conta do Google App Engine.
- Obtenha o Eclipse IDE para desenvolvedores de Java: descubra como esse popular IDE de Java simplifica o trabalho com o Google App Engine.
- Plug-in do Google para Eclipse: uma passagem rápida do desenvolvedor de Java pelo desenvolvimento de GAE.
- Tecnologias adicionais discutidas neste artigo:
***
artigo publicado originalmente por developerWorks Brasil, John Wheeler
John Wheeler está envolvido profissionalmente com programação há mais de uma década. Ele é o co-autor de Spring in Practice e trabalha para Xerox como gerente de aplicações. Visite o site de John para conhecer mais sobre seus artigos de desenvolvimento de software.