diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/CMSDHTMLEditor.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/CMSDHTMLEditor.java
new file mode 100755
index 000000000..d9ecdd622
--- /dev/null
+++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/CMSDHTMLEditor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.cms.ui;
+
+import com.arsdigita.bebop.form.DHTMLEditor;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.parameters.StringParameter;
+
+import org.arsdigita.cms.CMSConfig;
+import org.librecms.contentsection.ContentSection;
+
+/**
+ *
+ *
+ */
+public class CMSDHTMLEditor extends DHTMLEditor {
+
+ public CMSDHTMLEditor(final String name) {
+ super(new StringParameter(name),
+ CMSConfig.getConfig().getDHTMLEditorConfig());
+ addPlugins();
+ hideButtons();
+
+ }
+
+ public CMSDHTMLEditor(final ParameterModel model) {
+ super(model,
+ CMSConfig.getConfig().getDHTMLEditorConfig());
+
+ addPlugins();
+ hideButtons();
+ }
+
+ private void addPlugins() {
+
+ CMSConfig
+ .getConfig()
+ .getDhtmlEditorPlugins()
+ .forEach(plugin -> addPlugin(plugin));
+ }
+
+ private void hideButtons() {
+
+ CMSConfig
+ .getConfig()
+ .getDhtmlEditorHiddenButtons()
+ .forEach(hiddenButton -> hideButton(hiddenButton));
+ }
+
+}
diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/GenericArticleBody.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/GenericArticleBody.java
new file mode 100755
index 000000000..83dbdf9bf
--- /dev/null
+++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/GenericArticleBody.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.cms.ui.authoring;
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.RequestLocal;
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+import com.arsdigita.bebop.form.Option;
+import com.arsdigita.bebop.form.SingleSelect;
+import com.arsdigita.bebop.parameters.LongParameter;
+import com.arsdigita.cms.ItemSelectionModel;
+
+import org.librecms.contenttypes.Article;
+
+import com.arsdigita.cms.ui.workflow.WorkflowLockedComponentAccess;
+import com.arsdigita.util.Assert;
+
+import org.libreccm.cdi.utils.CdiUtil;
+import org.libreccm.l10n.LocalizedString;
+import org.librecms.contentsection.ContentItemRepository;
+
+/**
+ * Displays the current text body of the article and allows the user to edit it,
+ * by uploading a file or entering text in a text box.
+ *
+ * The {@link com.arsdigita.bebop.PropertySheet} class is often used as the
+ * display component in the default authoring kit steps of this class.
+ *
+ * @author Stanislav Freidin (sfreidin@arsdigita.com)
+ * @author Jens Pelzetter
+ */
+public class GenericArticleBody extends TextAssetBody {
+
+ protected AuthoringKitWizard authoringKitWizard;
+ protected ItemSelectionModel itemSelectionModel;
+
+ /**
+ * Construct a new GenericArticleBody component
+ *
+ * @param itemSelectionModel The {@link ItemSelectionModel} which will be
+ * responsible for loading the current item
+ *
+ * @param authoringKitWizard The parent wizard which contains the form. The
+ * form may use the wizard's methods, such as
+ * stepForward and stepBack, in its process
+ * listener.
+ */
+ public GenericArticleBody(final ItemSelectionModel itemSelectionModel,
+ final AuthoringKitWizard authoringKitWizard) {
+
+ super(new ItemAssetModel(null));
+ this.itemSelectionModel = itemSelectionModel;
+ this.authoringKitWizard = authoringKitWizard;
+
+ // Rest the component when it is hidden
+ authoringKitWizard
+ .getList()
+ .addActionListener(event -> reset(event.getPageState()));
+
+ // Set the right component access on the forms
+ final Component uploadComponent = getComponent(FILE_UPLOAD);
+ if (uploadComponent != null) {
+ setComponentAccess(FILE_UPLOAD,
+ new WorkflowLockedComponentAccess(
+ uploadComponent, itemSelectionModel));
+ }
+ final Component textEntryComponent = getComponent(TEXT_ENTRY);
+ setComponentAccess(TEXT_ENTRY,
+ new WorkflowLockedComponentAccess(
+ textEntryComponent, itemSelectionModel));
+ }
+
+ /**
+ * Adds the options for the mime type select widget of
+ * GenericArticleForm and sets the default mime type.
+ *
+ */
+ @Override
+ protected void setMimeTypeOptions(final SingleSelect mimeSelect) {
+ mimeSelect.addOption(new Option("text/html", "HTML Text"));
+ mimeSelect.setOptionSelected("text/html");
+ }
+
+ /**
+ * Create a new text asset and associate it with the current item
+ *
+ * @param state the current page state
+ *
+ * @return a valid TextAsset
+ */
+ @Override
+ protected LocalizedString createTextAsset(final PageState state) {
+
+ final Article article = getGenericArticle(state);
+ final LocalizedString text = article.getText();
+
+ // no need - cg. Text doesn't need a security context,
+ // and ownership of text is recorded in text_pages
+ // t.setParent(item);
+ return text;
+ }
+
+ /**
+ * Set additional parameters of a brand new text asset, such as the parent
+ * ID, after the asset has been successfully uploaded
+ *
+ * @param state the current page state
+ * @param text the new TextAsset
+ */
+ @Override
+ protected void updateTextAsset(final PageState state,
+ final LocalizedString text) {
+
+ final Article article = getGenericArticle(state);
+
+ // a.setParent(t);
+ article.setText(text);
+ final ContentItemRepository itemRepo = CdiUtil
+ .createCdiUtil()
+ .findBean(ContentItemRepository.class);
+ itemRepo.save(article);
+ }
+
+ /**
+ * Get the current GenericArticle
+ *
+ * @param state
+ */
+ protected Article getGenericArticle(final PageState state) {
+
+ return (Article) itemSelectionModel.getSelectedObject(state);
+ }
+
+ @Override
+ protected String getTextAssetName() {
+ return "text";
+ }
+
+ @Override
+ public LocalizedString getTextAsset(final PageState state) {
+ throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+ }
+
+ /**
+ * An ACSObjectSelectionModel that selects the current text asset for the
+ * text page
+ */
+ private static class ItemAssetModel extends ItemSelectionModel {
+
+ private RequestLocal requestLocal;
+
+ public ItemAssetModel(final LongParameter parameter) {
+
+ super(parameter);
+
+ requestLocal = new RequestLocal() {
+
+ @Override
+ protected Object initialValue(final PageState state) {
+// final Article t
+// = (Article) ((ItemSelectionModel) getSingleSelectionModel())
+// .getSelectedObject(state);
+// Assert.exists(t);
+// return t.getTextAsset();
+
+ throw new UnsupportedOperationException("ToDo");
+ }
+
+ };
+ }
+
+
+ @Override
+ public boolean isSelected(PageState s) {
+ return (getSelectedObject(s) != null);
+ }
+
+ }
+
+}
diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/TextAssetBody.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/TextAssetBody.java
new file mode 100755
index 000000000..de8d4407f
--- /dev/null
+++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/authoring/TextAssetBody.java
@@ -0,0 +1,740 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.cms.ui.authoring;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.logging.log4j.Logger;
+
+import com.arsdigita.bebop.BoxPanel;
+import com.arsdigita.bebop.ColumnPanel;
+import com.arsdigita.bebop.Form;
+import com.arsdigita.bebop.FormData;
+import com.arsdigita.bebop.FormProcessException;
+import com.arsdigita.bebop.Label;
+import com.arsdigita.bebop.Page;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.RequestLocal;
+import com.arsdigita.bebop.Resettable;
+import com.arsdigita.bebop.SaveCancelSection;
+import com.arsdigita.bebop.event.FormInitListener;
+import com.arsdigita.bebop.event.FormProcessListener;
+import com.arsdigita.bebop.event.FormSectionEvent;
+import com.arsdigita.bebop.event.FormSubmissionListener;
+import com.arsdigita.bebop.event.FormValidationListener;
+import com.arsdigita.bebop.event.RequestEvent;
+import com.arsdigita.bebop.event.RequestListener;
+import com.arsdigita.bebop.form.FormErrorDisplay;
+import com.arsdigita.bebop.form.SingleSelect;
+import com.arsdigita.bebop.form.TextArea;
+import com.arsdigita.bebop.parameters.NotNullValidationListener;
+import com.arsdigita.bebop.parameters.StringParameter;
+
+import org.librecms.contentsection.ContentSection;
+
+import com.arsdigita.cms.ItemSelectionModel;
+import com.arsdigita.cms.ui.CMSDHTMLEditor;
+import com.arsdigita.cms.ui.ContentItemPage;
+import com.arsdigita.cms.ui.FileUploadSection;
+import com.arsdigita.cms.ui.SecurityPropertyEditor;
+import com.arsdigita.globalization.Globalization;
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.toolbox.ui.DomainObjectPropertySheet;
+import com.arsdigita.util.UncheckedWrapperException;
+
+import org.apache.logging.log4j.LogManager;
+import org.arsdigita.cms.CMSConfig;
+import org.libreccm.l10n.LocalizedString;
+import org.librecms.CmsConstants;
+
+import java.io.IOException;
+
+import javax.activation.MimeType;
+
+/**
+ * Displays the mime-type and the body of a single {@code TextAsset}. Maintains
+ * a form or uploading files into the text body of the asset, and a form for
+ * editing the text of the asset.
+ *
+ * Unlike most other authoring components, this component does not require the
+ * asset to exist. If the asset does not exist (i.e., if
+ * !m_assetModel.isSelected(state)), the upload and editing forms
+ * will create a new asset and set it in the model by calling
+ * setSelectedObject on the asset selection model. Child classes
+ * should override the {@link #createTextAsset(PageState)} method in to create a
+ * valid text asset.
+ *
+ * This component is used primarily in {@link GenericArticleBody} and
+ * {@link com.arsdigita.cms.ui.templates.TemplateBody}
+ *
+ * Note: In CCM NG (version 7 and newer) {@code TextAsset} does not
+ * longer exist. Instead fields of type {@link LocalizedString} are used. This
+ * class has been adapted to use {@link LocalizedString}. The name of the class
+ * etc. has been kept to make the migration easier.
+ *
+ * @author Stanislav Freidin (sfreidin@arsdigita.com)
+ * @version Jens Pelzetter
+ */
+public abstract class TextAssetBody
+ extends SecurityPropertyEditor
+ implements Resettable, AuthoringStepComponent, RequestListener {
+
+ public static final String FILE_UPLOAD = "file";
+ public static final String TEXT_ENTRY = "text";
+
+ private static final String STREAMLINED = "_streamlined";
+ private static final String STREAMLINED_DONE = "1";
+ private static final CMSConfig CMS_CONFIG = CMSConfig.getConfig();
+
+ private final StringParameter streamlinedCreationParam;
+ private ItemSelectionModel assetModel;
+
+ /**
+ * Construct a new GenericArticleBody component
+ *
+ * @param assetModel The {@link ItemSelectionModel} which will be
+ * responsible for maintaining the current asset
+ */
+ public TextAssetBody(final ItemSelectionModel assetModel) {
+ this(assetModel, null);
+ }
+
+ /**
+ * Construct a new GenericArticleBody component
+ *
+ * @param assetModel The {@link ItemSelectionModel} which will be
+ * responsible for maintaining the current asset
+ * @param authoringKitWizard The parent wizard which contains the form. The
+ * form may use the wizard's methods, such as
+ * stepForward and stepBack, in its process
+ * listener.
+ */
+ public TextAssetBody(final ItemSelectionModel assetModel,
+ final AuthoringKitWizard authoringKitWizard) {
+
+ super();
+ this.assetModel = assetModel;
+
+ if (authoringKitWizard == null) {
+ streamlinedCreationParam = new StringParameter("item_body_done");
+ } else {
+ streamlinedCreationParam = new StringParameter(
+ String.format("%s_body_done",
+ authoringKitWizard
+ .getContentType()
+ .getContentItemClass()
+ .getName()));
+ }
+
+ if (!CMS_CONFIG.isHideTextAssetUploadFile()) {
+ final PageFileForm pageFileForm = getPageFileForm();
+ addFileWidgets(pageFileForm);
+ add(FILE_UPLOAD,
+ new GlobalizedMessage("cms.ui.upload", CmsConstants.CMS_BUNDLE),
+ pageFileForm,
+ pageFileForm.getSaveCancelSection().getCancelButton());
+ }
+
+ final PageTextForm pageTextForm = new PageTextForm();
+ addTextWidgets(pageTextForm);
+ add(TEXT_ENTRY,
+ new GlobalizedMessage("cms.ui.edit", CmsConstants.CMS_BUNDLE),
+ pageTextForm,
+ pageTextForm.getSaveCancelSection().getCancelButton());
+
+ // Specify full path to properties of the text asset
+ final DomainObjectPropertySheet sheet = getBodyPropertySheet(assetModel);
+ sheet.add(new GlobalizedMessage("cms.ui.authoring.body",
+ CmsConstants.CMS_BUNDLE),
+ getTextAssetName());
+
+ setDisplayComponent(sheet);
+
+ getDisplayPane().setClassAttr("invertedPropertyDisplay");
+
+ }
+
+ /**
+ * Determines the name of the property holding the text.
+ *
+ * @return The name of the property holding the text.
+ */
+ protected abstract String getTextAssetName();
+
+ protected DomainObjectPropertySheet getBodyPropertySheet(
+ final ItemSelectionModel assetModel) {
+
+ return new TextAssetBodyPropertySheet(assetModel);
+ }
+
+ /**
+ * Adds the options for the mime type select widget of
+ * GenericArticleForm and sets the default mime type.
+ *
+ */
+ protected void setMimeTypeOptions(SingleSelect mimeSelect) {
+ FileUploadSection.addMimeOptions(mimeSelect, "text");
+ mimeSelect.setOptionSelected("text/html");
+ }
+
+ /**
+ * To be overwritten by subclasses, should return the field.
+ *
+ * @param state
+ */
+ public abstract LocalizedString getTextAsset(final PageState state);
+
+ /**
+ * Reset this component to its original state
+ *
+ * @param state the current page state
+ */
+ @Override
+ public void reset(final PageState state) {
+ showDisplayPane(state);
+ }
+
+ // Create a text asset if it does not exist.
+ // This should probably be a method in GenericArticle ?
+ protected LocalizedString createOrGetTextAsset(
+ final ItemSelectionModel assetModel,
+ final PageState state) {
+ // Get the text asset or create a new one
+ LocalizedString text = getTextAsset(state);
+
+ if (text == null) {
+ text = createTextAsset(state);
+
+ }
+
+ return text;
+ }
+
+ /**
+ * Create a brand new TextAsset. Child classes should override
+ * this method to do the right thing. The default implementation creates a
+ * parent-less TextAsset with a unique name.
+ *
+ * @param state the current page state
+ *
+ * @return a valid TextAsset
+ */
+ protected abstract LocalizedString createTextAsset(PageState state);
+
+ /**
+ * Set additional parameters of a brand new text asset, such as the parent
+ * ID, after the asset has been successfully uploaded
+ *
+ * @param state the current page state
+ * @param text the new TextAsset
+ */
+ protected abstract void updateTextAsset(PageState state,
+ LocalizedString text);
+
+ /**
+ * Return the ItemSelectionModel which will be used to maintain
+ * the current text asset
+ *
+ * @return
+ */
+ public ItemSelectionModel getAssetSelectionModel() {
+ return assetModel;
+ }
+
+ /**
+ * Forward to the next step if the streamlined creation parameter is turned
+ * on _and_ the streamlined_creation global state parameter is set to
+ * 'active'
+ *
+ * @param state the PageState
+ */
+ protected void maybeForwardToNextStep(final PageState state) {
+ if (ContentItemPage.isStreamlinedCreationActive(state)
+ && !STREAMLINED_DONE.
+ equals(state.getValue(streamlinedCreationParam))) {
+ state.setValue(streamlinedCreationParam, STREAMLINED_DONE);
+ fireCompletionEvent(state);
+ }
+ }
+
+ /**
+ * Cancel streamlined creation for this step if the streamlined creation
+ * parameter is turned on _and_ the streamlined_creation global state param
+ * is set to 'active'
+ *
+ * @param state the PageState
+ */
+ protected void cancelStreamlinedCreation(final PageState state) {
+ if (ContentItemPage.isStreamlinedCreationActive(state)) {
+ state.setValue(streamlinedCreationParam, STREAMLINED_DONE);
+ }
+ }
+
+ /**
+ * Open the edit component if the streamlined creation parameter is turned
+ * on _and_ the streamlined_creation global state param is set to 'active'
+ *
+ * @param event
+ */
+ @Override
+ public void pageRequested(RequestEvent event) {
+
+ final PageState state = event.getPageState();
+
+ if (ContentItemPage.isStreamlinedCreationActive(state)
+ && !STREAMLINED_DONE.
+ equals(state.getValue(streamlinedCreationParam))) {
+ showComponent(state, TEXT_ENTRY);
+ }
+ //}
+
+ }
+
+ /**
+ * This is the form that is used to upload files. This method can be used so
+ * that a subclass can use their own subclass of PageFileForm.
+ *
+ * @return
+ */
+ protected PageFileForm getPageFileForm() {
+ return new PageFileForm();
+ }
+
+ /**
+ * A form for editing TextAsset items. Displays a "file upload" widget,
+ * auto-guesses mime type
+ */
+ public class PageFileForm extends Form
+ implements FormProcessListener, FormValidationListener {
+
+ private SaveCancelSection saveCancelSection;
+ private FileUploadSection fileUploadSection;
+ // Variables saved by validate for processing
+ private RequestLocal fileUploadContent;
+ private RequestLocal fileUploadUsedINSO;
+ /**
+ * The text entry widget
+ */
+ public static final String TEXT_ENTRY = "text_entry";
+
+ /**
+ * Construct a new PageFileForm
+ */
+ public PageFileForm() {
+ super("PageFileUpload", new BoxPanel(BoxPanel.VERTICAL));
+ setMethod(Form.POST);
+ setEncType("multipart/form-data");
+ }
+
+ protected String getFileUploadContent(PageState state) {
+ return (String) fileUploadContent.get(state);
+ }
+
+ /**
+ * Make sure that files of this type can be uploaded
+ */
+ private void validateFileType(final MimeType mime,
+ final boolean textType)
+ throws FormProcessException {
+
+// boolean validType = textType || ((mime instanceof TextMimeType)
+// && ((TextMimeType) mime).
+// allowINSOConvert().booleanValue());
+ boolean validType = true; //ToDo
+
+ if (!validType) {
+ throw new FormProcessException(new GlobalizedMessage(
+ "cms.ui.authoring.invalid_file_type",
+ CmsConstants.CMS_BUNDLE));
+ }
+
+// boolean insoWorks = MimeTypeStatus.getMimeTypeStatus().
+// getInsoFilterWorks().intValue() == 1;
+ boolean insoWorks = true; //ToDo
+
+ if (!textType && !insoWorks) {
+ // Can't convert. inso filter is not working. Give message.
+ throw new FormProcessException(new GlobalizedMessage(
+ "cms.ui.authoring.couldnt_convert_missing_inso",
+ CmsConstants.CMS_BUNDLE));
+ }
+ }
+
+ /**
+ * read in the content of the file (in bytes).
+ */
+ private byte[] readFileBytes(final File file)
+ throws FormProcessException {
+
+ byte[] fileBytes;
+ try (final FileInputStream fs = new FileInputStream(file)) {
+ fileBytes = new byte[fs.available()];
+ fs.read(fileBytes);
+ } catch (IOException ex) {
+ throw new FormProcessException(new GlobalizedMessage(
+ "cms.ui.authoring.unable_to_load_file",
+ CmsConstants.CMS_BUNDLE));
+ }
+ return fileBytes;
+ }
+
+ /**
+ * Convert bytes to String, possibly using INSO filter to convert to
+ * HTML type
+ */
+ private String convertBytes(final byte[] fileBytes,
+ final boolean textType,
+ final boolean[] usedInso)
+ throws FormProcessException {
+ String fileContent;
+ // If mime type is not text type, try to convert to html
+ if (!textType) {
+ fileContent = new String(fileBytes);
+ if (fileContent != null) {
+ // Converted successfully, flag type should be html
+ usedInso[0] = true;
+ } else {
+ throw new FormProcessException(
+ new GlobalizedMessage(
+ "cms.ui.authoring.couldnt_convert_inso_failed",
+ CmsConstants.CMS_BUNDLE));
+ }
+ } else {
+ // Text type, no need to convert
+ final String enc = "UTF-8";
+ try {
+ fileContent = new String(fileBytes, enc);
+ } catch (UnsupportedEncodingException ex) {
+ throw new UncheckedWrapperException(
+ "cannot convert to encoding "
+ + enc, ex);
+ }
+ usedInso[0] = false;
+ }
+ return fileContent;
+ }
+
+ /**
+ * Extract the contents of the HTML Body tag. (Done to prevent base and
+ * other header tags from interfering with page display).
+ */
+ private String extractHTMLBody(final String htmlText)
+ throws FormProcessException {
+
+ final String lowerCase = htmlText.toLowerCase();
+ int bodyStart = lowerCase.indexOf("