/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.debug; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import org.eclipse.dltk.rhino.dbgp.DBGPDebugger; import org.mozilla.javascript.RhinoException; import com.servoy.j2db.FormController; import com.servoy.j2db.IApplication; import com.servoy.j2db.IDebugWebClient; import com.servoy.j2db.IDesignerCallback; import com.servoy.j2db.IFormController; import com.servoy.j2db.IFormManagerInternal; import com.servoy.j2db.IMainContainer; import com.servoy.j2db.dataprocessing.IDataSet; import com.servoy.j2db.persistence.AbstractBase; import com.servoy.j2db.persistence.FlattenedForm; import com.servoy.j2db.persistence.Form; import com.servoy.j2db.persistence.IPersist; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.Solution; import com.servoy.j2db.persistence.SolutionMetaData; import com.servoy.j2db.plugins.ClientPluginAccessProvider; import com.servoy.j2db.plugins.IClientPluginAccess; import com.servoy.j2db.scripting.IExecutingEnviroment; import com.servoy.j2db.server.headlessclient.DummyMainContainer; import com.servoy.j2db.server.headlessclient.SessionClient; import com.servoy.j2db.server.headlessclient.WebFormManager; import com.servoy.j2db.server.shared.IDebugHeadlessClient; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.ILogLevel; import com.servoy.j2db.util.ServoyException; /** * Headless client when running from developer. * @author acostescu */ public class DebugHeadlessClient extends SessionClient implements IDebugHeadlessClient { //This is needed for mobile client launch with switch to service option . // switching to service in developer causes a required refresh in a separate thread triggered by activeSolutionChanged // , meanwhile after setActiveSolution is called the mobileClientDelegate opens the browser which causes a get to the service solution which is not fully loaded and debuggable public static Object activeSolutionRefreshLock = new Object(); public static class DebugWebFormManager extends WebFormManager implements DebugUtils.DebugUpdateFormSupport { public DebugWebFormManager(IApplication app, IMainContainer mainp) { super(app, mainp); } @Override protected void makeSolutionSettings(Solution s) { IApplication app = getApplication(); if (app instanceof IDebugWebClient) ((IDebugWebClient)app).onSolutionOpen(); super.makeSolutionSettings(s); } public void updateForm(Form form) { boolean isNew = !possibleForms.containsValue(form); boolean isDeleted = false; if (!isNew) { isDeleted = !((AbstractBase)form.getParent()).getAllObjectsAsList().contains(form); } updateForm(form, isNew, isDeleted); } /** * @param form * @param isNew * @param isDeleted */ private void updateForm(Form form, boolean isNew, boolean isDeleted) { if (isNew) { addForm(form, false); } else if (isDeleted) { Iterator<Entry<String, Form>> iterator = possibleForms.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Form> entry = iterator.next(); if (entry.getValue().equals(form)) { iterator.remove(); } } } else { // just changed if (possibleForms.get(form.getName()) == null) { // name change, first remove the form updateForm(form, false, true); // then add it back in updateForm(form, true, false); } } } } private final Runnable checkForChangesRunnable = new Runnable() { public void run() { checkForChanges(); } }; private SolutionMetaData solution; private final IDesignerCallback designerCallBack; public DebugHeadlessClient(ServletRequest req, String name, String pass, String method, Object[] methodArgs, SolutionMetaData solution, IDesignerCallback designerCallBack) throws Exception { super(req, name, pass, method, methodArgs, solution == null ? null : solution.getName()); this.solution = solution; this.designerCallBack = designerCallBack; } @Override protected IClientPluginAccess createClientPluginAccess() { return new ClientPluginAccessProvider(this) { @Override public Object executeMethod(String context, String methodname, Object[] arguments, boolean async) throws Exception { checkForChanges(); return super.executeMethod(context, methodname, arguments, async); } }; } @Override protected IFormManagerInternal createFormManager() { DebugWebFormManager fm = new DebugWebFormManager(this, new DummyMainContainer(this)); return fm; } @Override protected void loadSolution(SolutionMetaData solutionMeta) throws RepositoryException { synchronized (activeSolutionRefreshLock) { if (!isShutDown()) { // ignore given always load the active. if (getSolution() != null) { closeSolution(true, null); } if (solution != null) { super.loadSolution(solution); } } } } @Override public void shutDown(boolean force) { synchronized (activeSolutionRefreshLock) { super.shutDown(force); } } @Override public boolean closeSolution(boolean force, Object[] args) { synchronized (activeSolutionRefreshLock) { return super.closeSolution(force, args); } } public void setCurrent(Solution s) { SolutionMetaData solutionMeta = (s == null) ? null : s.getSolutionMetaData(); synchronized (activeSolutionRefreshLock) { solution = solutionMeta; if (isSolutionLoaded() && (solutionMeta != null && !getSolution().getName().equals(solutionMeta.getName()) || solutionMeta == null)) { closeSolution(true, null); } } } private Form form; private final List<List<IPersist>> changesQueue = new ArrayList<List<IPersist>>(); private boolean performRefresh() { boolean changed = changesQueue.size() > 0; while (changesQueue.size() > 0) { performRefresh(changesQueue.remove(0)); } return changed; } private void performRefresh(List<IPersist> changes) { Set<IFormController>[] scopesAndFormsToReload = DebugUtils.getScopesAndFormsToReload(this, changes); refreshI18NMessages(); for (IFormController controller : scopesAndFormsToReload[0]) { if (controller.getForm() instanceof FlattenedForm) { FlattenedForm ff = (FlattenedForm)controller.getForm(); ff.reload(); } controller.getFormScope().reload(); } if (scopesAndFormsToReload[1].size() > 0) ((WebFormManager)getFormManager()).reload((scopesAndFormsToReload[1]).toArray(new FormController[0])); } public void refreshForI18NChange(boolean recreateForms) { List<IFormController> cachedFormControllers = getFormManager().getCachedFormControllers(); ArrayList<IPersist> formsToReload = new ArrayList<IPersist>(); for (IFormController fc : cachedFormControllers) formsToReload.add(fc.getForm()); refreshPersists(formsToReload); } /** * @param changes */ public void refreshPersists(Collection<IPersist> changes) { changesQueue.add(new ArrayList<IPersist>(changes)); } /** * @see com.servoy.j2db.server.headlessclient.SessionClient#createScriptEngine() */ @Override protected IExecutingEnviroment createScriptEngine() { RemoteDebugScriptEngine engine = new RemoteDebugScriptEngine(this); if (designerCallBack != null) { designerCallBack.addScriptObjects(this, engine.getSolutionScope()); } return engine; } /** * @see com.servoy.j2db.smart.J2DBClient#output(java.lang.Object) */ @Override public void output(Object msg, int level) { super.output(msg, level); if (level == ILogLevel.WARNING || level == ILogLevel.ERROR) { errorToDebugger(msg.toString(), null); } else { stdoutToDebugger(msg); } } protected void stdoutToDebugger(Object message) { DBGPDebugger debugger = getDebugger(); if (debugger != null) { debugger.outputStdOut((message == null ? "<null>" : message.toString()) + '\n'); } else { Debug.error("No debugger found, for msg report: " + message); } } private DBGPDebugger getDebugger() { RemoteDebugScriptEngine rdse = (RemoteDebugScriptEngine)getScriptEngine(); if (rdse == null) return null; return rdse.getDebugger(); } /** * @see com.servoy.j2db.smart.J2DBClient#reportJSError(java.lang.String, java.lang.Object) */ @Override public void reportJSError(String message, Object detail) { errorToDebugger(message, detail); super.reportJSError(message, detail); } /** * @see com.servoy.j2db.ClientState#reportError(java.lang.String, java.lang.Object) */ @Override public void reportError(String message, Object detail) { errorToDebugger(message, detail); super.reportError(message, detail); } /** * @param message * @param detail */ private void errorToDebugger(String message, Object errorDetail) { Object detail = errorDetail; RemoteDebugScriptEngine engine = (RemoteDebugScriptEngine)getScriptEngine(); if (engine != null) { DBGPDebugger debugger = engine.getDebugger(); if (debugger != null) { RhinoException rhinoException = null; if (detail instanceof Exception) { Throwable exception = (Exception)detail; while (exception != null) { if (exception instanceof RhinoException) { rhinoException = (RhinoException)exception; break; } exception = exception.getCause(); } } String msg = message; if (rhinoException != null) { if (msg == null) { msg = rhinoException.getLocalizedMessage(); } else msg += '\n' + rhinoException.getLocalizedMessage(); msg += '\n' + rhinoException.getScriptStackTrace(); } else if (detail instanceof Exception) { Object e = ((Exception)detail).getCause(); if (e != null) { detail = e; } msg += "\n > " + detail.toString(); // complete stack? if (detail instanceof ServoyException && ((ServoyException)detail).getScriptStackTrace() != null) { msg += '\n' + ((ServoyException)detail).getScriptStackTrace(); } } else if (detail != null) { msg += "\n" + detail; } debugger.outputStdErr(msg.toString() + '\n'); } } } /** * @param f */ public void show(Form f) { this.form = f; } /** * @param object */ public boolean checkForChanges() { boolean changed = false; if (getSolution() == null && solution != null) { try { loadSolution(solution); changed = true; } catch (Exception e) { Debug.error(e); } } if (!changed) { changed = performRefresh(); } if (getSolution() != null && form != null) { getFormManager().showFormInMainPanel(form.getName()); form = null; changed = true; } return changed; } @Override public synchronized boolean setMainForm(String formName) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.setMainForm(formName); } @Override public synchronized Object getDataProviderValue(String contextName, String dataprovider) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.getDataProviderValue(contextName, dataprovider); } @Override public synchronized Object setDataProviderValue(String contextName, String dataprovider, Object value) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.setDataProviderValue(contextName, dataprovider, value); } @Override public synchronized int setDataProviderValues(String contextName, HttpServletRequest request_data) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.setDataProviderValues(contextName, request_data); } @Override public synchronized void saveData() { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding super.saveData(); } @Override public synchronized Object executeMethod(String visibleFormName, String methodName, Object[] arguments) throws Exception { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.executeMethod(visibleFormName, methodName, arguments); } @Override public synchronized String getI18NMessage(String key, Object[] args) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.getI18NMessage(key, args); } @Override public synchronized void setLocale(Locale l) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding super.setLocale(l); } @Override public synchronized IDataSet getValueListItems(String contextName, String valuelistName) { invokeAndWaitIfExecutionNotLocked(checkForChangesRunnable); // this is an ISessionBean interface method that can be called from JSP - update things before proceeding return super.getValueListItems(contextName, valuelistName); } private void invokeAndWaitIfExecutionNotLocked(Runnable r) { if (!isExecutionLocked()) super.invokeAndWait(r); } }