package railo.runtime.tag;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import railo.commons.io.IOUtil;
import railo.commons.io.compress.ZipUtil;
import railo.commons.io.res.Resource;
import railo.commons.io.res.filter.DirectoryResourceFilter;
import railo.commons.io.res.filter.FileResourceFilter;
import railo.commons.io.res.filter.OrResourceFilter;
import railo.commons.io.res.filter.ResourceFilter;
import railo.commons.io.res.util.FileWrapper;
import railo.commons.io.res.util.ResourceUtil;
import railo.commons.io.res.util.WildcardPatternFilter;
import railo.commons.lang.StringUtil;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.ext.tag.BodyTagImpl;
import railo.runtime.op.Caster;
import railo.runtime.op.Decision;
import railo.runtime.type.QueryImpl;
import railo.runtime.type.dt.DateTimeImpl;
public final class Zip extends BodyTagImpl {
private String action="zip";
private String charset;
private Resource destination;
private String entryPath;
private Resource file;
private WildcardPatternFilter filter;
private String name;
private boolean overwrite;
private String prefix;
private boolean recurse=true;
private boolean showDirectory;
private boolean storePath=true;
private String variable;
private List<ZipParamAbstr> params;
private Set<String> alreadyUsed;
private Resource source;
private static int id=0;
@Override
public void release() {
super.release();
action="zip";
charset=null;
destination=null;
entryPath=null;
file=null;
filter=null;
name=null;
overwrite=false;
prefix=null;
recurse=true;
showDirectory=false;
source=null;
storePath=true;
variable=null;
if(params!=null)params.clear();
if(alreadyUsed!=null)alreadyUsed.clear();
}
/**
* @param action the action to set
*/
public void setAction(String action) {
this.action = action.trim().toLowerCase();
}
/**
* @param charset the charset to set
*/
public void setCharset(String charset) {
this.charset = charset;
}
/**
* @param destination the destination to set
* @throws ExpressionException
* @throws PageException
*/
public void setDestination(String strDestination) throws PageException {
this.destination = ResourceUtil.toResourceExistingParent(pageContext, strDestination);
if(!destination.exists())destination.mkdirs();
if(!destination.isDirectory())
throw new ApplicationException("destination ["+strDestination+"] is not a existing directory");
}
/**
* @param entryPath the entryPath to set
*/
public void setEntrypath(String entryPath) {
if(StringUtil.isEmpty(entryPath,true)) return;
entryPath=entryPath.trim();
entryPath=entryPath.replace('\\','/');
if(StringUtil.startsWith(entryPath,'/'))entryPath=entryPath.substring(1);
if(StringUtil.endsWith(entryPath,'/'))entryPath=entryPath.substring(0,entryPath.length()-1);
this.entryPath = entryPath;
}
/**
* @param file the file to set
* @throws ExpressionException
*/
public void setFile(String file) {
this.file = ResourceUtil.toResourceNotExisting(pageContext, file);
}
/**
* @param filter the filter to set
*/
public void setFilter(String filter) {
if ( !filter.isEmpty() )
this.filter = new WildcardPatternFilter( filter );
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @param overwrite the overwrite to set
*/
public void setOverwrite(boolean overwrite) {
this.overwrite = overwrite;
}
/**
* @param prefix the prefix to set
*/
public void setPrefix(String prefix) {
this.prefix = prefix;
}
/**
* @param recurse the recurse to set
*/
public void setRecurse(boolean recurse) {
this.recurse = recurse;
}
/**
* @param showDirectory the showDirectory to set
*/
public void setShowdirectory(boolean showDirectory) {
this.showDirectory = showDirectory;
}
/**
* @param source the source to set
* @throws PageException
*/
public void setSource(String strSource) throws PageException {
source = ResourceUtil.toResourceExisting(pageContext, strSource);
}
/**
* @param storePath the storePath to set
*/
public void setStorepath(boolean storePath) {
this.storePath = storePath;
}
/**
* @param variable the variable to set
*/
public void setVariable(String variable) {
this.variable = variable;
}
@Override
public int doStartTag() throws PageException {
return EVAL_BODY_INCLUDE;
}
private void actionDelete() throws ApplicationException, IOException {
required("file",file,true);
Resource existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName());
IOUtil.copy(file, existing);
ZipInputStream zis = null;
ZipOutputStream zos = null;
try {
zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) );
zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream()));
ZipEntry entry;
String path,name;
int index;
boolean accept;
if(filter==null && recurse && entryPath==null)
throw new ApplicationException("define at least one restriction, can't delete all the entries from a zip file");
while ( ( entry = zis.getNextEntry()) != null ) {
accept=false;
path = entry.getName().replace('\\', '/');
index=path.lastIndexOf('/');
if(!recurse && index>0) accept=true;
//dir=index==-1?"":path.substring(0,index);
name=path.substring(index+1);
if(filter!=null && !filter.accept(name)) accept=true;
if(!entryPathMatch(path)) accept=true;
if(!accept) continue;
add(zos, entry, zis, false);
zis.closeEntry();
}
}
finally {
IOUtil.closeEL(zis);
IOUtil.closeEL(zos);
existing.delete();
}
}
private void actionList() throws PageException, IOException {
required("file",file,true);
required("name",name);
railo.runtime.type.Query query=new QueryImpl(
new String[]{"name","size","type","dateLastModified","directory","crc","compressedSize","comment"},
0,"query");
pageContext.setVariable(name, query);
ZipFile zip = getZip(file);
Enumeration entries = zip.entries();
try {
String path,name,dir;
ZipEntry ze;
int row=0,index;
while(entries.hasMoreElements()) {
ze = (ZipEntry)entries.nextElement();
if(!showDirectory && ze.isDirectory()) continue;
path = ze.getName().replace('\\', '/');
index=path.lastIndexOf('/');
if(!recurse && index>0) continue;
dir=index==-1?"":path.substring(0,index);
name=path.substring(index+1);
if(filter!=null && !filter.accept(null, name)) continue;
if(!entryPathMatch(dir)) continue;
//if(entryPath!=null && !(dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/"))) ;///continue;
row++;
query.addRow();
query.setAt("name", row, path);
query.setAt("size", row, Caster.toDouble(ze.getSize()));
query.setAt("type", row, ze.isDirectory()?"Directory":"File");
query.setAt("dateLastModified", row, new DateTimeImpl(pageContext,ze.getTime(),false));
query.setAt("crc", row, Caster.toDouble(ze.getCrc()));
query.setAt("compressedSize", row, Caster.toDouble(ze.getCompressedSize()));
query.setAt("comment", row, ze.getComment());
query.setAt("directory", row, dir);
//zis.closeEntry();
}
}
finally {
IOUtil.closeEL(zip);
}
}
private boolean entryPathMatch(String dir) {
if(entryPath==null) return true;
return dir.equalsIgnoreCase(entryPath) || StringUtil.startsWithIgnoreCase(dir,entryPath+"/");
}
private void actionRead(boolean binary) throws ZipException, IOException, PageException {
required("file",file,true);
required("variable",variable);
required("entrypath",variable);
ZipFile zip = getZip(file);
try {
ZipEntry ze = zip.getEntry(entryPath);
if(ze==null)ze = zip.getEntry(entryPath+"/");
if(ze==null) throw new ApplicationException("zip file ["+file+"] has no entry with name ["+entryPath+"]");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = zip.getInputStream(ze);
IOUtil.copy(is, baos,true,false);
zip.close();
if(binary)
pageContext.setVariable(variable, baos.toByteArray());
else {
if(charset==null)charset=pageContext.getConfig().getResourceCharset();
pageContext.setVariable(variable, new String(baos.toByteArray(),charset));
}
}
finally {
IOUtil.closeEL(zip);
}
}
private void actionUnzip() throws ApplicationException, IOException {
required("file",file,true);
required("destination",destination,false);
ZipInputStream zis=null;
String path;
Resource target,parent;
int index;
try {
zis = new ZipInputStream( IOUtil.toBufferedInputStream(file.getInputStream()) ) ;
ZipEntry entry;
while ( ( entry = zis.getNextEntry()) != null ) {
path = entry.getName().replace('\\', '/');
index=path.lastIndexOf('/');
// recurse
if(!recurse && index!=-1) {
zis.closeEntry();
continue;
}
// filter
if(filter!=null && !filter.accept(path.substring(index+1))) {
zis.closeEntry();
continue;
}
// entrypath
if(!entryPathMatch(path)) {
zis.closeEntry();
continue;
}
target=destination.getRealResource(entry.getName());
if(!storePath) target=destination.getRealResource(target.getName());
if(entry.isDirectory()) {
target.mkdirs();
}
else {
if(storePath){
parent=target.getParentResource();
if(!parent.exists())parent.mkdirs();
}
if(overwrite || !target.exists())IOUtil.copy(zis,target,false);
}
target.setLastModified(entry.getTime());
zis.closeEntry() ;
}
}
finally {
IOUtil.closeEL(zis);
}
}
private void actionZip() throws PageException, IOException {
required("file",file,false);
Resource dir = file.getParentResource();
if(!dir.exists()) {
throw new ApplicationException("directory ["+dir.toString()+"] doesn't exist");
}
if((params==null || params.isEmpty()) && source!=null) {
setParam(new ZipParamSource(source,entryPath,filter,prefix,recurse));
}
if((params==null || params.isEmpty())) {
throw new ApplicationException("No source/content specified");
}
ZipOutputStream zos=null;
Resource existing=null;
try {
// existing
if(!overwrite && file.exists()) {
existing = pageContext.getConfig().getTempDirectory().getRealResource(getTempName());
IOUtil.copy(file, existing);
}
zos = new ZipOutputStream(IOUtil.toBufferedOutputStream(file.getOutputStream()));
Object[] arr = params.toArray();
for(int i=arr.length-1;i>=0;i--) {
if(arr[i] instanceof ZipParamSource)
actionZip(zos,(ZipParamSource)arr[i]);
else if(arr[i] instanceof ZipParamContent)
actionZip(zos,(ZipParamContent)arr[i]);
}
if(existing!=null) {
ZipInputStream zis = new ZipInputStream( IOUtil.toBufferedInputStream(existing.getInputStream()) );
try {
ZipEntry entry;
while ( ( entry = zis.getNextEntry()) != null ) {
add(zos, entry, zis, false);
zis.closeEntry();
}
}
finally {
zis.close();
}
}
}
finally {
ZipUtil.close(zos);
if(existing!=null)existing.delete();
}
}
private String getTempName() {
return "tmp-"+(id++)+".zip";
}
private void actionZip(ZipOutputStream zos, ZipParamContent zpc) throws PageException, IOException {
Object content = zpc.getContent();
if(Decision.isBinary(content)) {
add(zos, new ByteArrayInputStream(Caster.toBinary(content)), zpc.getEntryPath(), System.currentTimeMillis(), true);
}
else {
String charset=zpc.getCharset();
if(StringUtil.isEmpty(charset))charset=pageContext.getConfig().getResourceCharset();
add(zos, new ByteArrayInputStream(content.toString().getBytes(charset)), zpc.getEntryPath(), System.currentTimeMillis(), true);
}
}
private void actionZip(ZipOutputStream zos, ZipParamSource zps) throws IOException {
// prefix
String p=zps.getPrefix();
if(StringUtil.isEmpty(p))
p=this.prefix;
if(!StringUtil.isEmpty(p)){
if(!StringUtil.endsWith(p, '/'))p+="/";
}
else
p="";
if(zps.getSource().isFile()){
String ep = zps.getEntryPath();
if(ep==null)ep=zps.getSource().getName();
if(!StringUtil.isEmpty(p)) ep=p+ep;
add(zos,zps.getSource().getInputStream(),ep,zps.getSource().lastModified(),true);
}
else {
// filter
ResourceFilter f = zps.getFilter();
if(f==null)f=this.filter;
if(zps.isRecurse()) {
if(f!=null)f=new OrResourceFilter(new ResourceFilter[]{DirectoryResourceFilter.FILTER,f});
}
else {
if(f==null)f=FileResourceFilter.FILTER;
}
addDir(zos,zps.getSource(),p,f);
}
}
private void addDir(ZipOutputStream zos, Resource dir, String parent, ResourceFilter filter) throws IOException {
Resource[] children = filter==null?dir.listResources():dir.listResources(filter);
for(int i=0;i<children.length;i++) {
if(children[i].isDirectory()) addDir(zos, children[i], parent+children[i].getName()+"/",filter);
else {
add(zos, children[i].getInputStream(), parent+children[i].getName(), children[i].lastModified(), true);
}
}
}
private void add(ZipOutputStream zos, InputStream is, String path, long lastMod, boolean closeInput) throws IOException {
ZipEntry ze=new ZipEntry(path);
ze.setTime(lastMod);
add(zos, ze, is, closeInput);
}
private void add(ZipOutputStream zos, ZipEntry entry,InputStream is, boolean closeInput) throws IOException {
if(alreadyUsed==null)alreadyUsed=new HashSet<String>();
else if(alreadyUsed.contains(entry.getName())) return;
zos.putNextEntry(entry);
try {
IOUtil.copy(is,zos,closeInput,false);
}
finally {
zos.closeEntry();
}
alreadyUsed.add(entry.getName());
}
@Override
public void doInitBody() {
}
@Override
public int doAfterBody() {
return SKIP_BODY;
}
@Override
public int doEndTag() throws PageException {//print.out("doEndTag"+doCaching+"-"+body);
try {
if(action.equals("delete")) actionDelete();
else if(action.equals("list")) actionList();
else if(action.equals("read")) actionRead(false);
else if(action.equals("readbinary")) actionRead(true);
else if(action.equals("unzip")) actionUnzip();
else if(action.equals("zip")) actionZip();
else
throw new ApplicationException("invalid value ["+action+"] for attribute action","values for attribute action are:info,move,rename,copy,delete,read,readbinary,write,append,upload");
}
catch(IOException ioe) {
throw Caster.toPageException(ioe);
}
return EVAL_PAGE;
}
/**
* sets if tag has a body or not
* @param hasBody
*/
public void hasBody(boolean hasBody) {
///this.hasBody=hasBody;
}
private ZipFile getZip(Resource file) throws ZipException, IOException {
return new ZipFile(FileWrapper.toFile(file));
}
/**
* throw a error if the value is empty (null)
* @param attributeName
* @param atttributValue
* @throws ApplicationException
*/
private void required(String attributeName, String attributValue) throws ApplicationException {
if(StringUtil.isEmpty(attributValue))
throw new ApplicationException(
"invalid attribute constellation for the tag zip",
"attribute ["+attributeName+"] is required, if action is ["+action+"]");
}
/**
* throw a error if the value is empty (null)
* @param attributeName
* @param atttributValue
* @throws ApplicationException
*/
private void required(String attributeName, Resource attributValue, boolean exists) throws ApplicationException {
if(attributValue==null)
throw new ApplicationException(
"invalid attribute constellation for the tag zip",
"attribute ["+attributeName+"] is required, if action is ["+action+"]");
if(exists && !attributValue.exists())
throw new ApplicationException(attributeName+" resource ["+attributValue+"] doesn't exist");
else if(exists && !attributValue.canRead())
throw new ApplicationException("no access to "+attributeName+" resource ["+attributValue+"]");
}
public void setParam(ZipParamAbstr param) {
if(params==null) {
params=new ArrayList<ZipParamAbstr>();
alreadyUsed=new HashSet<String>();
}
params.add(param);
}
/**
* @return the source
*/
public Resource getSource() {
return source;
}
}