//========================================================================
//$Id: IJetty.java 474 2012-01-23 03:07:14Z janb.webtide $
//Copyright 2008 Mort Bay Consulting Pty. Ltd.
//------------------------------------------------------------------------
//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 org.mortbay.ijetty;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import org.eclipse.jetty.util.IO;
import org.mortbay.ijetty.log.AndroidLog;
import org.mortbay.ijetty.util.AndroidInfo;
import org.mortbay.ijetty.util.IJettyToast;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.text.Html;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* IJetty
*
* Main Jetty activity. Can start other activities: + configure + download
*
* Can start/stop services: + IJettyService
*/
public class IJetty extends Activity
{
private static final String TAG = "Jetty";
public static final String __START_ACTION = "org.mortbay.ijetty.start";
public static final String __STOP_ACTION = "org.mortbay.ijetty.stop";
public static final String __PORT = "org.mortbay.ijetty.port";
public static final String __NIO = "org.mortbay.ijetty.nio";
public static final String __SSL = "org.mortbay.ijetty.ssl";
public static final String __CONSOLE_PWD = "org.mortbay.ijetty.console";
public static final String __PORT_DEFAULT = "8080";
public static final boolean __NIO_DEFAULT = true;
public static final boolean __SSL_DEFAULT = false;
public static final String __CONSOLE_PWD_DEFAULT = "admin";
public static final String __WEBAPP_DIR = "webapps";
public static final String __ETC_DIR = "etc";
public static final String __CONTEXTS_DIR = "contexts";
public static final String __TMP_DIR = "tmp";
public static final String __WORK_DIR = "work";
public static final int __SETUP_PROGRESS_DIALOG = 0;
public static final int __SETUP_DONE = 2;
public static final int __SETUP_RUNNING = 1;
public static final int __SETUP_NOTDONE = 0;
public static final File __JETTY_DIR;
private Button startButton;
private Button stopButton;
private Button configButton;
private TextView footer;
private TextView info;
private TextView console;
private ScrollView consoleScroller;
private StringBuilder consoleBuffer = new StringBuilder();
private Runnable scrollTask;
private ProgressDialog progressDialog;
private Thread progressThread;
private Handler handler;
private BroadcastReceiver bcastReceiver;
class ConsoleScrollTask implements Runnable
{
public void run()
{
consoleScroller.fullScroll(View.FOCUS_DOWN);
}
}
/**
* ProgressThread
*
* Handles finishing install tasks for Jetty.
*/
class ProgressThread extends Thread
{
private Handler _handler;
public ProgressThread(Handler h) {
_handler = h;
}
public void sendProgressUpdate (int prog)
{
Message msg = _handler.obtainMessage();
Bundle b = new Bundle();
b.putInt("prog", prog);
msg.setData(b);
_handler.sendMessage(msg);
}
public void run ()
{
boolean updateNeeded = isUpdateNeeded();
//create the jetty dir structure
File jettyDir = __JETTY_DIR;
if (!jettyDir.exists())
{
boolean made = jettyDir.mkdirs();
Log.i(TAG,"Made " + __JETTY_DIR + ": " + made);
}
sendProgressUpdate(10);
//Do not make a work directory to preserve unpacked
//webapps - this seems to clash with Android when
//out-of-date webapps are deleted and then re-unpacked
//on a jetty restart: Android remembers where the dex
//file of the old webapp was installed, but it's now
//been replaced by a new file of the same name. Strangely,
//this does not seem to affect webapps unpacked to tmp?
//Original versions of i-jetty created a work directory. So
//we will delete it here if found to ensure webapps can be
//updated successfully.
File workDir = new File(jettyDir, __WORK_DIR);
if (workDir.exists())
{
Installer.delete(workDir);
Log.i(TAG, "removed work dir");
}
//make jetty/tmp
File tmpDir = new File(jettyDir,__TMP_DIR);
if (!tmpDir.exists())
{
boolean made = tmpDir.mkdirs();
Log.i(TAG,"Made " + tmpDir + ": " + made);
}
else
{
Log.i(TAG,tmpDir + " exists");
}
//make jetty/webapps
File webappsDir = new File(jettyDir,__WEBAPP_DIR);
if (!webappsDir.exists())
{
boolean made = webappsDir.mkdirs();
Log.i(TAG,"Made " + webappsDir + ": " + made);
try
{
InputStream is = getAssets().open("webapps_greenstone.jar");
Installer.install(is, "/greenstone3", webappsDir, "greenstone3", false);
Log.i("Jetty", "Loaded console webapp");
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
else
{
Log.i(TAG,webappsDir + " exists");
}
//make jetty/etc
File etcDir = new File(jettyDir,__ETC_DIR);
if (!etcDir.exists())
{
boolean made = etcDir.mkdirs();
Log.i(TAG,"Made " + etcDir + ": " + made);
}
else
{
Log.i(TAG,etcDir + " exists");
}
sendProgressUpdate(30);
File webdefaults = new File(etcDir,"webdefault.xml");
if (!webdefaults.exists() || updateNeeded)
{
//get the webdefaults.xml file out of resources
try
{
InputStream is = getResources().openRawResource(R.raw.webdefault);
OutputStream os = new FileOutputStream(webdefaults);
IO.copy(is,os);
Log.i(TAG,"Loaded webdefault.xml");
}
catch (Exception e)
{
Log.e(TAG,"Error loading webdefault.xml",e);
}
}
sendProgressUpdate(40);
File realm = new File(etcDir,"realm.properties");
if (!realm.exists() || updateNeeded)
{
try
{
//get the realm.properties file out resources
InputStream is = getResources().openRawResource(R.raw.realm_properties);
OutputStream os = new FileOutputStream(realm);
IO.copy(is,os);
Log.i(TAG,"Loaded realm.properties");
}
catch (Exception e)
{
Log.e(TAG,"Error loading realm.properties",e);
}
}
sendProgressUpdate(50);
File keystore = new File(etcDir,"keystore");
if (!keystore.exists() || updateNeeded)
{
try
{
//get the keystore out of resources
InputStream is = getResources().openRawResource(R.raw.keystore);
OutputStream os = new FileOutputStream(keystore);
IO.copy(is,os);
Log.i(TAG,"Loaded keystore");
}
catch (Exception e)
{
Log.e(TAG,"Error loading keystore",e);
}
}
sendProgressUpdate(60);
//make jetty/contexts
File contextsDir = new File(jettyDir,__CONTEXTS_DIR);
if (!contextsDir.exists())
{
boolean made = contextsDir.mkdirs();
Log.i(TAG,"Made " + contextsDir + ": " + made);
}
else
{
Log.i(TAG,contextsDir + " exists");
}
sendProgressUpdate(70);
try
{
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(),0);
if (pi != null)
{
setStoredJettyVersion(pi.versionCode);
}
}
catch (Exception e)
{
Log.w(TAG, "Unable to get PackageInfo for i-jetty");
}
//if there was a .update file indicating an update was needed, remove it now we've updated
File update = new File(__JETTY_DIR, ".update");
if (update.exists())
update.delete();
sendProgressUpdate(100);
}
};
static
{
__JETTY_DIR = new File(Environment.getExternalStorageDirectory(),"jetty");
// Ensure parsing is not validating - does not work with android
System.setProperty("org.eclipse.jetty.xml.XmlParser.Validating","false");
// Bridge Jetty logging to Android logging
System.setProperty("org.eclipse.jetty.util.log.class","org.mortbay.ijetty.AndroidLog");
org.eclipse.jetty.util.log.Log.setLog(new AndroidLog());
}
public IJetty ()
{
super();
handler = new Handler ()
{
public void handleMessage(Message msg) {
int total = msg.getData().getInt("prog");
progressDialog.setProgress(total);
if (total >= 100){
dismissDialog(__SETUP_PROGRESS_DIALOG);
}
}
};
}
public String formatJettyInfoLine (String format, Object ... args)
{
String ms = "";
if (format != null)
ms = String.format(format, args);
return ms+"
";
}
public void consolePrint(String format, Object... args)
{
String msg = String.format(format,args);
if (msg.length() > 0)
{
consoleBuffer.append(msg).append("
");
console.setText(Html.fromHtml(consoleBuffer.toString()));
Log.i(TAG,msg); // Only interested in non-empty lines being output to Log
}
else
{
consoleBuffer.append(msg).append("
");
console.setText(Html.fromHtml(consoleBuffer.toString()));
}
if (scrollTask == null)
{
scrollTask = new ConsoleScrollTask();
}
consoleScroller.post(scrollTask);
}
protected int getStoredJettyVersion()
{
File jettyDir = __JETTY_DIR;
if (!jettyDir.exists())
{
return -1;
}
File versionFile = new File(jettyDir,"version.code");
if (!versionFile.exists())
{
return -1;
}
int val = -1;
ObjectInputStream ois = null;
try
{
ois = new ObjectInputStream(new FileInputStream(versionFile));
val = ois.readInt();
return val;
}
catch (Exception e)
{
Log.e(TAG,"Problem reading version.code",e);
return -1;
}
finally
{
if (ois != null)
{
try
{
ois.close();
}
catch (Exception e)
{
Log.d(TAG,"Error closing version.code input stream",e);
}
}
}
}
@Override
protected void onDestroy()
{
if (bcastReceiver != null)
unregisterReceiver(bcastReceiver);
super.onDestroy();
}
@Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
setContentView(R.layout.jetty_controller);
startButton = (Button)findViewById(R.id.start);
stopButton = (Button)findViewById(R.id.stop);
configButton = (Button)findViewById(R.id.config);
final Button downloadButton = (Button)findViewById(R.id.download);
IntentFilter filter = new IntentFilter();
filter.addAction(__START_ACTION);
filter.addAction(__STOP_ACTION);
filter.addCategory("default");
bcastReceiver =
new BroadcastReceiver()
{
public void onReceive(Context context, Intent intent)
{
if (__START_ACTION.equalsIgnoreCase(intent.getAction()))
{
startButton.setEnabled(false);
configButton.setEnabled(false);
stopButton.setEnabled(true);
consolePrint("
Started Jetty at %s", new Date());
String[] connectors = intent.getExtras().getStringArray("connectors");
if (null != connectors)
{
for (int i=0;i Jetty stopped at %s",new Date());
}
}
};
registerReceiver(bcastReceiver, filter);
// Watch for button clicks.
startButton.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
if (isUpdateNeeded())
IJettyToast.showQuickToast(IJetty.this,R.string.loading);
else
{
//TODO get these values from editable UI elements
Intent intent = new Intent(IJetty.this,IJettyService.class);
intent.putExtra(__PORT,__PORT_DEFAULT);
intent.putExtra(__NIO,__NIO_DEFAULT);
intent.putExtra(__SSL,__SSL_DEFAULT);
intent.putExtra(__CONSOLE_PWD,__CONSOLE_PWD_DEFAULT);
startService(intent);
}
}
});
stopButton.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
stopService(new Intent(IJetty.this,IJettyService.class));
}
});
configButton.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
IJettyEditor.show(IJetty.this);
}
});
downloadButton.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
IJettyDownloader.show(IJetty.this);
}
});
info = (TextView)findViewById(R.id.info);
footer = (TextView)findViewById(R.id.footer);
console = (TextView)findViewById(R.id.console);
consoleScroller = (ScrollView)findViewById(R.id.consoleScroller);
StringBuilder infoBuffer = new StringBuilder();
try
{
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(),0);
infoBuffer.append(formatJettyInfoLine ("i-jetty version %s (%s)",pi.versionName,pi.versionCode));
}
catch (NameNotFoundException e)
{
infoBuffer.append(formatJettyInfoLine ("i-jetty version unknown"));
}
infoBuffer.append(formatJettyInfoLine("On %s using Android version %s",AndroidInfo.getDeviceModel(), AndroidInfo.getOSVersion()));
info.setText(Html.fromHtml(infoBuffer.toString()));
/*
StringBuilder footerBuffer = new StringBuilder();
footerBuffer.append("Project: http://code.google.com/p/i-jetty
");
footerBuffer.append("Server: http://www.eclipse.org/jetty
");
footerBuffer.append("Support: http://www.intalio.com/jetty/services
");
footer.setText(Html.fromHtml(footerBuffer.toString()));
*/
}
public static void show(Context context)
{
final Intent intent = new Intent(context,IJetty.class);
context.startActivity(intent);
}
@Override
protected void onResume()
{
if (!SdCardUnavailableActivity.isExternalStorageAvailable())
{
SdCardUnavailableActivity.show(this);
}
else
{
//work out if we need to do the installation finish step
//or not. We do it iff:
// - there is no previous jetty version on disk
// - the previous version does not match the current version
// - we're not already doing the update
if (isUpdateNeeded())
{
setupJetty();
}
}
if (IJettyService.isRunning())
{
startButton.setEnabled(false);
configButton.setEnabled(false);
stopButton.setEnabled(true);
}
else
{
startButton.setEnabled(true);
configButton.setEnabled(true);
stopButton.setEnabled(false);
}
super.onResume();
}
@Override
protected Dialog onCreateDialog(int id)
{
switch(id)
{
case __SETUP_PROGRESS_DIALOG:
{
progressDialog = new ProgressDialog(IJetty.this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMessage("Finishing initial install ...");
return progressDialog;
}
default:
return null;
}
}
private void printNetworkInterfaces()
{
try
{
Enumeration nis = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni : Collections.list(nis))
{
Enumeration iis = ni.getInetAddresses();
for (InetAddress ia : Collections.list(iis))
{
consoleBuffer.append(formatJettyInfoLine("Network interface: %s: %s",ni.getDisplayName(),ia.getHostAddress()));
}
}
}
catch (SocketException e)
{
Log.w(TAG, e);
}
}
protected void setStoredJettyVersion(int version)
{
File jettyDir = __JETTY_DIR;
if (!jettyDir.exists())
{
return;
}
File versionFile = new File(jettyDir,"version.code");
ObjectOutputStream oos = null;
try
{
FileOutputStream fos = new FileOutputStream(versionFile);
oos = new ObjectOutputStream(fos);
oos.writeInt(version);
oos.flush();
}
catch (Exception e)
{
Log.e(TAG,"Problem writing jetty version",e);
}
finally
{
if (oos != null)
{
try
{
oos.close();
}
catch (Exception e)
{
Log.d(TAG,"Error closing version.code output stream",e);
}
}
}
}
/**
* We need to an update iff we don't know the current
* jetty version or it is different to the last version
* that was installed.
*
* @return
*/
public boolean isUpdateNeeded ()
{
//if no previous version file, assume update is required
int storedVersion = getStoredJettyVersion();
if (storedVersion <= 0)
return true;
try
{
//if different previous version, update is required
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(),0);
if (pi == null)
return true;
if (pi.versionCode != storedVersion)
return true;
//if /sdcard/jetty/.update file exists, then update is required
File alwaysUpdate = new File(__JETTY_DIR,".update");
if (alwaysUpdate.exists())
{
Log.i(TAG,"Always Update tag found " + alwaysUpdate);
return true;
}
}
catch (Exception e)
{
//if any of these tests go wrong, best to assume update is true?
return true;
}
return false;
}
public void setupJetty()
{
showDialog(__SETUP_PROGRESS_DIALOG);
progressThread = new ProgressThread(handler);
progressThread.start();
};
}