/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* Caucho Technology permits redistribution, modification and use
* of this file in source and binary form ("the Software") under the
* Caucho Developer Source License ("the License"). The following
* conditions must be met:
*
* 1. Each copy or derived work of the Software must preserve the copyright
* notice and this notice unmodified.
*
* 2. Redistributions of the Software in source or binary form must include
* an unmodified copy of the License, normally in a plain ASCII text
*
* 3. The names "Resin" or "Caucho" are trademarks of Caucho Technology and
* may not be used to endorse products derived from this software.
* "Resin" or "Caucho" may not appear in the names of products derived
* from this software.
*
* This Software is provided "AS IS," without a warranty of any kind.
* ALL EXPRESS OR IMPLIED REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
* IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
*
* CAUCHO TECHNOLOGY AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES
* SUFFERED BY LICENSEE OR ANY THIRD PARTY AS A RESULT OF USING OR
* DISTRIBUTING SOFTWARE. IN NO EVENT WILL CAUCHO OR ITS LICENSORS BE LIABLE
* FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
* CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
* REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
* INABILITY TO USE SOFTWARE, EVEN IF HE HAS BEEN ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGES.
*
* @author Sam
*/
package com.caucho.doc.javadoc;
import com.caucho.config.ConfigException;
import com.caucho.config.types.Period;
import com.caucho.loader.Environment;
import com.caucho.log.Log;
import com.caucho.server.webapp.WebApp;
import com.caucho.server.webapp.PathMapping;
import com.caucho.util.CharBuffer;
import com.caucho.util.Crc64;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/**
* A store for javadoc information.
*/
public class Store {
static protected final Logger log = Log.open(Store.class);
static final L10N L = new L10N(Store.class);
private final static String STORE_JNDINAME = "resin-javadoc/store";
private String _dataSource;
private LinkedHashMap<String,Api> _api = new LinkedHashMap<String,Api>();
private Path _timestampFile;
private String _tableNameFile = "javadoc_file";
private String _tableNameItem = "javadoc_item";
private long _httpCachePeriod;
private String _startPage = "overview.jsp";
private boolean _showAddHelp;
private long _crc64 = 0;
private DataSource _pool;
private Exception _initError;
/**
* A convenience method to do the JNDI lookup
*/
static public Store getInstance()
throws NamingException
{
Context env = (Context) new InitialContext().lookup("java:comp/env");
Store store = (Store) env.lookup(STORE_JNDINAME);
if (store == null)
throw new NamingException(L.l("`{0}' is an unknown Store",STORE_JNDINAME));
return store;
}
/**
* The data-source indicates the database to use.
*/
public void setDataSource(String dataSource)
{
_dataSource = dataSource;
}
/**
* A timestamp file is used to determine if the database needs to be
* recreated from the javadoc, default "WEB-INF/timestamp".
*/
public void setTimestampFile(Path timestampFile)
{
_timestampFile = timestampFile;
}
/**
* An api is a javadoc generated set of html files.
*/
public void addApi(Api api)
throws ConfigException
{
if (_api.get(api.getId()) != null)
throw new ConfigException(L.l("id `{0}' already used, id must be unique",api.getId()));
_api.put(api.getId(),api);
}
/**
* An api is a javadoc generated set of html files.
*/
public Api getApi(String id)
{
return _api.get(id);
}
/**
* An api is a javadoc generated set of html files.
*/
public Collection<Api> getAllApi()
{
return _api.values();
}
/**
* The table name for storing information about javadoc items, the default is
* "javadoc_item".
*/
public void setTableNameItem(String tableNameItem)
{
_tableNameItem = tableNameItem;
}
/**
* The table name for storing information about javadoc items.
*/
public String getTableNameItem()
{
return _tableNameItem;
}
/**
* The table name for storing information about javadoc files, the default is
* "javadoc_file".
*/
public void setTableNameFile(String tableNameFile)
{
_tableNameFile = tableNameFile;
}
/**
* The table name for storing information about javadoc files.
*/
public String getTableNameFile()
{
return _tableNameFile;
}
/**
* The time period to indicate to Resin's cache and to browsers
* that the responses (including search results) should be cached.
* If Resin's http cache is enabled, then the cached
* result will be used for subsequent searches for the same thing, even
* by a different user. This is an effective way to avoid hitting the
* database for every search.
*
* examples: 10m, 10h, 10D,
* -1 disables (not a good idea, but useful during development).
*/
public void setHttpCachePeriod(Period httpCachePeriod)
{
_httpCachePeriod = httpCachePeriod.getPeriod();
}
/**
* The time period for cache expiry, in ms.
*/
public long getHttpCachePeriod()
{
return _httpCachePeriod;
}
/**
* The page to show in the class window for the first request, default
* "overview.jsp".
*/
public void setStartPage(String startPage)
{
_startPage = startPage;
}
/**
* The page to show in the class window for the first request.
*/
public String getStartPage()
{
return _startPage;
}
/**
* True/false show a help message about adding more api's.
*/
public void setShowAddHelp(boolean showAddHelp)
{
_showAddHelp = showAddHelp;
}
/**
* True/false show a help message about adding more api's.
*/
public boolean getShowAddHelp()
{
return _showAddHelp;
}
public void init()
{
try {
if (_timestampFile == null)
_timestampFile = Vfs.lookup("WEB-INF/timestamp");
try {
Context env = (Context) new InitialContext().lookup("java:comp/env");
_pool = (DataSource) env.lookup(_dataSource);
if (_pool == null)
throw new ConfigException(L.l("`{0}' is an unknown DataSource, database has not been configured or is not configured properly.",_dataSource));
} catch (NamingException e) {
throw new ConfigException(e);
}
// update database if needed
for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
Api api = i.next();
_crc64 = api.generateCrc64(_crc64);
}
_crc64 = Crc64.generate(_crc64,_dataSource);
_crc64 = Crc64.generate(_crc64,_timestampFile.toString());
_crc64 = Crc64.generate(_crc64,_tableNameFile);
_crc64 = Crc64.generate(_crc64,_tableNameItem);
try {
if (isNeedUpdate())
createFromIndex();
} catch (Exception ex) {
throw new ConfigException(ex);
}
// add path-mappings to map local files with absolute paths
WebApp app = WebApp.getLocal();
CharBuffer cb = CharBuffer.allocate();
for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
Api api = i.next();
if (api.isLocalAbsolute()) {
cb.clear();
cb.append("/");
cb.append(api.getId());
cb.append("/*");
String urlPattern = cb.toString();
PathMapping pm = new PathMapping();
pm.setUrlPattern(urlPattern);
pm.setRealPath(api.getLocation());
try {
app.addPathMapping(pm);
} catch (Exception ex) {
throw new ConfigException(ex);
}
}
}
cb.free();
// add dependencies to the Environment so that if a local api
// is regenerated the web-app is restarted
for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
Api api = i.next();
if (api.isLocal()) {
Path index = api.getLocationPath().lookup("index.html");
Environment.addDependency(index);
}
}
} catch (Exception ex) {
log.log(Level.FINE,"resin-javadoc init error",ex);
_initError = ex;
}
}
public Exception getInitError()
{
return _initError;
}
/**
* True if the timestamp file indicates that the database needs updating to
* match the index file(s).
*/
public boolean isNeedUpdate()
{
try {
ReadStream rs = _timestampFile.openRead();
String lms = rs.readLine();
long crc = Long.parseLong(lms);
lms = rs.readLine();
long tsfiles = Long.parseLong(lms);
rs.close();
if (crc != _crc64) {
log.info(L.l("javadoc index needs update - config has changed"));
if (log.isLoggable(Level.FINE))
log.finer(L.l("timestamp file {0} lastmodified {1}",_timestampFile,new Long(crc)));
}
else if (tsfiles != getTimestampForLocalFiles()) {
log.info(L.l("javadoc index needs update - a local api has changed"));
}
else {
return false;
}
} catch (Exception ex) {
log.info(L.l("javadoc index needs update - timestamp file could not be read"));
if (log.isLoggable(Level.FINE))
log.finer(L.l("timestamp file {0} {1}",_timestampFile,ex));
}
return true;
}
/**
* Clear the database tables (if they exist) and create new
* contents based on the passed javadoc index file(s).
*/
public void createFromIndex()
throws SQLException, IOException
{
long st = System.currentTimeMillis();
int cnt = 0;
log.info(L.l("creating javadoc index db entries"));
Connection conn = null;
try {
conn = _pool.getConnection();
StoreWriter sw = new StoreWriter(this);
sw.clear(conn);
for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
Api api = i.next();
cnt += sw.add(conn,api);
}
} finally {
try {
if (conn != null)
conn.close();
} catch (SQLException e) {
log.warning(L.l("conn.close() error",e));
}
}
updateTimestampFile();
long tm = System.currentTimeMillis() - st;
log.info(L.l("created {0} javadoc index entries in {1}sec", new Integer(cnt), new Double( (double) tm / 1000.0)));
}
private long getTimestampForLocalFiles()
{
long ts = 0L;
for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
Api api = i.next();
if (api.isLocal()) {
ts += api.getLocationPath().lookup("index.html").getLastModified();
}
}
return ts;
}
private void updateTimestampFile()
throws IOException
{
_timestampFile.getParent().mkdirs();
WriteStream ws = _timestampFile.openWrite();
ws.println(_crc64);
ws.println(getTimestampForLocalFiles());
ws.close();
}
/**
* Look for a javadoc item. The string can begin with `package', `class',
* `method', `var', or `any'. If it begins with none of them, `any' is
* assumed. The rest of the string is used as the name to search for.
* If the name conatins the `*' character, the `*' will match any
* characters.
*
* @returns a list of the results, a list with size 0 for no results
*/
public LinkedList<JavadocItem> query(String query, int offset, int limit)
throws SQLException
{
LinkedList<JavadocItem> results = new LinkedList<JavadocItem>();
try {
int type = -1;
int i = query.indexOf(' ');
if (i > -1) {
String t = query.substring(0,i);
if (t.equals("package"))
type = JavadocItem.PACKAGE;
else if (t.equals("class"))
type = JavadocItem.CLASS;
else if (t.equals("method"))
type = JavadocItem.METHOD;
else if (t.equals("var"))
type = JavadocItem.VARIABLE;
else if (t.equals("any"))
type = JavadocItem.ANY;
if (type > -1) {
while (i < query.length() && Character.isWhitespace(query.charAt(i)))
i++;
if (i >= query.length())
return results;
else
query = query.substring(i);
}
}
// handle the special case of ClassName.method
if (query.length() > 0 && Character.isUpperCase(query.charAt(0))) {
int di = query.indexOf('.');
if (di > -1 ) {
CharBuffer cb = CharBuffer.allocate();
cb.append('*');
cb.append(query);
cb.append('*');
query = cb.close();
}
}
if (type < 0)
type = JavadocItem.ANY;
Connection conn = null;
try {
conn = _pool.getConnection();
Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
// construct the query or queries
boolean like = query.indexOf("*") > -1 ? true : false;
String nameToUse = (query.indexOf(".") > -1) ? "item.fullname" : "item.name";
String safequery = escapeSQL(query);
CharBuffer cb = CharBuffer.allocate();
cb.append("SELECT item.name,item.fullname,item.typ,item.anchor,item.description,file.api,file.path FROM ");
cb.append(getTableNameItem());
cb.append(" AS item, ");
cb.append(getTableNameFile());
cb.append(" AS file WHERE file.id = item.file_id");
if (type != JavadocItem.ANY) {
cb.append(" AND item.typ = ");
cb.append(type);
}
String q_select = cb.close();
if (like) {
cb = CharBuffer.allocate();
cb.append(q_select);
cb.append(" AND ");
cb.append(nameToUse);
cb.append(" LIKE '");
cb.append(safequery);
cb.append("'");
results = doQuery(safequery,stmt,cb,offset,limit,results);
}
else {
cb = CharBuffer.allocate();
cb.append(q_select);
cb.append(" AND ");
cb.append(nameToUse);
cb.append(" LIKE '");
cb.append(safequery);
cb.append("%'");
results = doQuery(safequery,stmt,cb,offset,limit,results);
}
// see if the first one is exact
if (results.size() > 0) {
JavadocItem item = results.getFirst();
if (item.getName().equals(query) || item.getFullName().equals(query)) {
item.setExact(true);
}
}
stmt.close();
} finally {
if (conn != null)
conn.close();
}
}
catch (SQLException ex) {
if (log.isLoggable(Level.CONFIG))
log.log(Level.CONFIG,L.l("error with query `{0}': {1}",query,ex.getMessage()));
throw ex;
}
return results;
}
private String escapeSQL(String q)
{
CharBuffer cb = CharBuffer.allocate();
for (int i = 0; i < q.length(); i++) {
char c = q.charAt(i);
switch (c) {
case '\'':
cb.append("\\'");
break;
case '\"':
cb.append("\\\"");
break;
case '*':
cb.append("%");
break;
case '%':
cb.append('\\');
default:
cb.append(c);
}
}
return cb.close();
}
private LinkedList<JavadocItem> doQuery(String query, Statement stmt, CharBuffer cb, int offset, int limit, LinkedList<JavadocItem> results)
throws SQLException
{
if (limit <= 0) {
return results;
}
cb.append(" ORDER BY if(item.name = '");
cb.append(query);
cb.append("' OR item.fullname = '");
cb.append(query);
cb.append("',0,1) ");
cb.append(",item.typ");
cb.append(", LENGTH(item.name)");
cb.append(" LIMIT ");
cb.append(limit);
if (offset > 0) {
cb.append(" OFFSET ");
cb.append(offset);
}
String q = cb.close();
log.finest(L.l("query is [{0}]",q));
ResultSet rs = stmt.executeQuery(q);
try {
while (rs.next()) {
results.add(new JavadocItem(
rs.getString(1), // name
rs.getString(2), // fullname
rs.getInt(3), // type
rs.getString(4), // anchor
rs.getString(5), // description
new JavadocFile(_api.get(rs.getString(6)), // api
rs.getString(7)) // path
));
}
} finally {
rs.close();
}
return results;
}
}