/**
*
*/
package org.sinnlabs.dbvim.zk;
import java.io.StringReader;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.sinnlabs.dbvim.db.Database;
import org.sinnlabs.dbvim.db.DatabaseFactory;
import org.sinnlabs.dbvim.db.Entry;
import org.sinnlabs.dbvim.db.Value;
import org.sinnlabs.dbvim.db.exceptions.DatabaseOperationException;
import org.sinnlabs.dbvim.db.model.IDBField;
import org.sinnlabs.dbvim.evaluator.exceptions.ParseException;
import org.sinnlabs.dbvim.form.FormFieldResolver;
import org.sinnlabs.dbvim.form.FormFieldResolverFactory;
import org.sinnlabs.dbvim.model.Form;
import org.sinnlabs.dbvim.model.ResultColumn;
import org.sinnlabs.dbvim.script.ScriptApi;
import org.sinnlabs.dbvim.ui.IField;
import org.sinnlabs.dbvim.ui.annotations.EventType;
import org.sinnlabs.dbvim.ui.db.ConditionFieldMenuitem;
import org.sinnlabs.dbvim.ui.events.VimEvents;
import org.sinnlabs.dbvim.zk.model.FormEventProcessor;
import org.sinnlabs.dbvim.zk.model.IFormComposer;
import org.springframework.context.annotation.Scope;
import org.springframework.security.access.prepost.PreAuthorize;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.MouseEvent;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.VariableResolver;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Button;
import org.zkoss.zul.Hlayout;
import org.zkoss.zul.Idspace;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listfooter;
import org.zkoss.zul.Listheader;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Menupopup;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.North;
import org.zkoss.zul.South;
import org.zkoss.zul.Textbox;
import org.zkoss.zul.Toolbarbutton;
import org.zkoss.zul.impl.InputElement;
/**
* Class implements zk composer
*
* @author peter.liverovsky
*
*/
@VariableResolver(org.zkoss.zkplus.spring.DelegatingVariableResolver.class)
public class SearchComposer extends SelectorComposer<Component> implements IFormComposer {
/**
*
*/
private static final long serialVersionUID = 8674446600172735254L;
protected static final int MODE_SEARCH = 1;
protected static final int MODE_RESULT = 2;
protected static final int MODE_CREATE = 3;
protected static final int MODE_CHANGE = 4;
@Wire("#border #searchResults")
North searchResults;
@Wire("#border #center")
Idspace detailsView;
@Wire("#border #btnSearch")
Button btnSearch;
@Wire("#lstResults")
Listbox results;
@Wire("#btnNewSearch")
Toolbarbutton btnNewSearch;
@Wire("#btnSave")
Toolbarbutton btnSave;
@Wire("#btnChangeAll")
Toolbarbutton btnChange;
@Wire("#btnCopyToNew")
Toolbarbutton btnCopy;
@Wire("#btnDelete")
Toolbarbutton btnDelete;
@Wire("#btnCreate")
Toolbarbutton btnCreate;
@Wire("#btnNewEntry")
Toolbarbutton btnNewQuery;
@Wire("#btnAdditionalSearch")
Toolbarbutton btnAdditionalSearch;
@Wire("#lstFooterTotal")
Listfooter lstFooter;
@Wire("#divSearch")
Hlayout divSearch;
@Wire("#divNewEntry")
Hlayout divNewEntry;
@Wire("#divModify")
Hlayout divModify;
@Wire("#divChange")
Hlayout divChange;
@Wire("#south")
South south;
@Wire("#txtAdditionalSearch")
Textbox txtAdditionalSearch;
@Wire("#btnFields")
Button btnFields;
/**
* Current form
*/
Form form;
/**
* Current form resolver
*/
FormFieldResolver resolver;
/**
* Form events processor
*/
FormEventProcessor eventProcessor;
/**
* Current entry
*/
Entry currentEntry;
/**
* List of all form fields
*/
List<Component> fieldList;
/**
* Contains all form fields
*/
List<IField<?>> fields;
/**
* List of all read only fields
*/
List<Component> readonlyFields;
/**
* Database object for the current form
*/
Database db;
/**
* Api for the scripting
*/
ScriptApi api;
/**
* Menu contains all form fields
*/
protected Menupopup fieldsPopup;
List<Value<?>> lastSearch;
private LastSearch search;
private int currentViewMode;
private boolean isAdditional = false;
public List<IField<?>> getFields() { return fields; }
public ScriptApi getApi() { return api; }
private class LastSearch {
public List<Value<?>> values;
public String additional;
}
public ComponentInfo doBeforeCompose(Page page, Component parent, ComponentInfo compInfo) {
// We must do initialization before ui will be created
// initialize all for related objects
search = new LastSearch();
form = (Form) Executions.getCurrent().getArg().get("form");
// init field lists
fieldList = new ArrayList<Component>();
fields = new ArrayList<IField<?>>();
eventProcessor = new FormEventProcessor();
// init script api object
api = new ScriptApi(this);
return super.doBeforeCompose(page, parent, compInfo);
}
public void doAfterCompose(Component comp) throws Exception {
// Initialize components
try {
resolver = FormFieldResolverFactory.getResolver(form);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
try {
db = DatabaseFactory.createInstance(form, resolver);
} catch (ClassNotFoundException | DatabaseOperationException
| SQLException e) {
e.printStackTrace();
throw e;
}
super.doAfterCompose(comp);
// create form ui
loadForm();
currentEntry = null;
// build search result headers
results.getListhead().setSizable(true);
results.setMultiple(true);
if (form.getResultList() != null) {
for(ResultColumn column : form.getResultList()) {
Listheader header = new Listheader();
header.setSort("auto");
header.setLabel(column.label);
results.getListhead().appendChild(header);
}
}
/** Invoke form loaded event **/
try {
eventProcessor.Invoke(EventType.FORM_LOADED, (Object[]) null);
} catch (Exception e) {
Messagebox.show("Unable to raise onFormLoaded event.", "error", Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
setMode(MODE_SEARCH);
/** init fields popup menu **/
initFieldsMenu();
/** process request parameters **/
@SuppressWarnings("unchecked")
Map<String, String[]> params =
(Map<String, String[]>) Executions.getCurrent().getArg().get("params");
if (params != null) {
if (params.containsKey("query")) {
String query = params.get("query")[0];
isAdditional = true;
txtAdditionalSearch.setText(query);
search(new ArrayList<Value<?>>());
} else if (params.containsKey("mode")) {
String formMode = params.get("mode")[0];
if (formMode.equals("create")) {
setMode(MODE_CREATE);
} else if (formMode.equals("search")) {
setMode(MODE_SEARCH);
}
/** populate fields **/
for (IField<?> f : fields) {
String s = "'" + f.getId() + "'";
if (params.containsKey(s)) {
f.setValueFromString((params.get(s)[0]));
} else if (params.containsKey(f.getId())) {
f.setValueFromString(params.get(f.getId())[0]);
}
}
}
}
}
/**
* Initialize field select menu
*/
private void initFieldsMenu() {
fieldsPopup = new Menupopup();
fieldsPopup.setStyle("overflow: auto; max-height: 100vh;");
detailsView.appendChild(fieldsPopup);
for (IField<?> c : fields) {
final ConditionFieldMenuitem i = new ConditionFieldMenuitem(c);
i.setLabel(c.getId() + " (" + c.getLabel() + ")");
/** Add item event listener **/
i.addEventListener(Events.ON_CLICK, new EventListener<MouseEvent>() {
@Override
public void onEvent(MouseEvent arg0) throws Exception {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() +
"'" + i.getField().getId() + "'");
}
});
/** add item **/
fieldsPopup.appendChild(i);
}
}
@Listen("onClick = #btnFields")
public void btnFields_onClick() {
if (fieldsPopup != null) {
fieldsPopup.open(btnFields);
}
}
@Listen("onClick = #btnEq")
public void btnEq_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " = ");
}
@Listen("onClick = #btnNotEq")
public void btnNotEq_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " != ");
}
@Listen("onClick = #btnLt")
public void btnLt_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " < ");
}
@Listen("onClick = #btnGt")
public void btnGt_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " > ");
}
@Listen("onClick = #btnLtEq")
public void btnLtEq_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " <= ");
}
@Listen("onClick = #btnGtEq")
public void btnGtEq_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " >= ");
}
@Listen("onClick = #btnLIKE")
public void btnLIKE_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " LIKE ");
}
@Listen("onClick = #btnAND")
public void btnAND_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " AND ");
}
@Listen("onClick = #btnOR")
public void btnOR_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " OR ");
}
@Listen("onClick = #btnNOT")
public void btnNOT_onClick() {
txtAdditionalSearch.setText(txtAdditionalSearch.getText() + " NOT ");
}
@Listen("onClick = #btnSearch")
public void btnSearch_onClick() {
List<Value<?>> userValues = getUserValues();
lastSearch = userValues;
search(lastSearch);
}
@Listen("onSelect = #lstResults")
public void lstResults_onSelect() {
Entry e = results.getSelectedItem().getValue();
setMode(MODE_RESULT);
try {
currentEntry = db.readEntry(e);
} catch (DatabaseOperationException ex) {
Messagebox.show("DB Operation error: " + ex.getMessage(), "ERROR",
Messagebox.OK, Messagebox.ERROR);
ex.printStackTrace();
return;
}
populateFields();
raiseOnEntryLoadedEvent();
}
@Listen("onClick = #btnNewSearch")
public void btnNewSearch_onClick() {
setMode(MODE_SEARCH);
clearAllFields(detailsView);
}
@Listen("onClick = #btnAdditionalSearch")
public void btnAdditionalSearch_onClick() {
setAdditionalSearch(!isAdditional);
}
@Listen("onClick = #btnNewEntry")
public void btnNewEntry_onClick() {
setMode(MODE_CREATE);
clearAllFields(detailsView);
}
@Listen("onClick = #btnChangeAll")
public void btnChangeAll_onClick() {
setMode(MODE_CHANGE);
clearAllFields(detailsView);
}
@Listen("onClick = #btnSave")
public void btnSave_onClick() {
if (currentEntry != null && currentViewMode == MODE_RESULT) {
if (scanForNulls()) {
return;
}
// get values
List<Value<?>> values = new ArrayList<Value<?>>();
for(Component c : fieldList) {
if (!((IField<?>)c).isDisplayOnly())
values.add(((IField<?>)c).getDBValue());
}
//update entry
try {
db.updateEntry(currentEntry, values);
} catch (DatabaseOperationException e) {
Messagebox.show("Unable to update entry.", "Update error.",
Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
return;
}
// mark as modified
if (results.getSelectedItem() != null) {
markItemAsChanged(results.getSelectedItem());
}
} else if (currentViewMode == MODE_CHANGE) {
Messagebox.show("You are shure that you want to update " + this.results.getItemCount() +
" entries.",
"Update entries", Messagebox.YES | Messagebox.NO, Messagebox.QUESTION,
new EventListener<Event>() {
@Override
public void onEvent(Event evnt) throws Exception {
if (evnt.getName().equals(Messagebox.ON_YES)) {
updateAllEntries();
}
}
});
}
}
private void markItemAsChanged(Listitem item) {
for (Component c : item.getChildren()) {
if (c instanceof Listcell) {
((Listcell) c).setStyle("font-style: italic;");
}
}
}
@Listen("onClick = #btnCreate")
public void btnCreate_onClick() {
if (scanForNulls()) {
return;
}
// get values
Entry e = new Entry();
for(Component c : fieldList) {
IField<?> f = (IField<?>) c;
if (f.getDBValue().getValue() != null && !f.isDisplayOnly())
e.getValues().add(f.getDBValue());
}
// if no user values
if (e.getValues().size() == 0)
return;
// create new entry
try {
db.insertEntry(e);
clearAllFields(detailsView);
} catch (DatabaseOperationException e1) {
Messagebox.show("Unable to create entry");
e1.printStackTrace();
}
}
@Listen("onClick = #btnDelete")
public void btnDelete_onClick() {
if (currentEntry != null) {
Messagebox.show("You are shure that you want to delete entries?", "Delete",
Messagebox.YES|Messagebox.NO, Messagebox.QUESTION, new EventListener<Event>() {
@Override
public void onEvent(Event evnt) throws Exception {
if (evnt.getName().equals(Messagebox.ON_YES)) {
try {
for(Listitem i : results.getSelectedItems()) {
Entry e = i.getValue();
db.deleteEntry(e);
}
} catch (DatabaseOperationException e) {
Messagebox.show("Unable to delte entry.", "Error",
Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
search(lastSearch);
}
}
});
}
}
/**
* Updates all fields from result list with value
* @throws ParseException
* @throws DatabaseOperationException
*/
private void updateAllEntries() throws ParseException, DatabaseOperationException {
if (search == null)
return;
// get the user values
List<Value<?>> values = getUserValues();
// update the records
if (search.values == null && search.additional != null) {
db.update(values, search.additional, null);
} else if (search.values != null && search.additional == null) {
db.update(search.values, values);
} else if (search.values == null && search.additional == null) {
db.updateAll(values);
}
// mark all result list items as changed
for (Listitem i : results.getItems()) {
markItemAsChanged(i);
}
}
/**
* Perform search on the form
* @param values - User entered values, that can be used for filtering
*/
private void search(List<Value<?>> values) {
List<Entry> entries = null;
if (values == null)
return;
try {
if (isAdditional) {
List<IField<?>> fields = new ArrayList<IField<?>>();
for(Component c : fieldList) {
fields.add((IField<?>) c);
}
entries = db.query(null, txtAdditionalSearch.getText(), 0, null);
search.values = null;
search.additional = txtAdditionalSearch.getText();
} else if (isAdditional == false && values.size() == 0) {
entries = db.queryAll(null, 0);
search.values = null;
search.additional = null;
} else {
entries = db.query(null, values, 0);
search.values = values;
search.additional = null;
}
} catch(DatabaseOperationException e) {
Messagebox.show("DB Operation error: " + e.getMessage(), "ERROR",
Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
return;
} catch (WrongValueException e1) {
Messagebox.show(e1.getMessage(), "ERROR",
Messagebox.OK, Messagebox.ERROR);
e1.printStackTrace();
} catch (ParseException e1) {
Messagebox.show(e1.getMessage(), "ERROR",
Messagebox.OK, Messagebox.ERROR);
e1.printStackTrace();
}
results.getItems().clear();
if (entries == null || entries.size() == 0) {
Messagebox.show("No etries found.");
return;
}
for(Entry e : entries) {
Listitem item = new Listitem();
item.setValue(e);
for (Value<?> v : e.getValues()) {
Listcell cell = new Listcell();
if (v.getValue() != null)
cell.setLabel(v.getValue().toString());
item.appendChild(cell);
}
results.getItems().add(item);
}
lstFooter.setLabel(results.getItemCount() + " entries");
// select first item
if (results.getItemCount() > 0) {
results.setSelectedIndex(0);
currentEntry = results.getSelectedItem().getValue();
try {
currentEntry = db.readEntry(currentEntry);
} catch (DatabaseOperationException e) {
Messagebox.show("DB Operation error: " + e.getMessage(), "ERROR",
Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
return;
}
populateFields();
}
setMode(MODE_RESULT);
raiseOnEntryLoadedEvent();
}
/**
* Checks that all not nullable fields has the assigned value.
* Shows notification to user if not nullable field set to null
* @return False if all not nullable fields has a value, otherwise True
*/
private boolean scanForNulls() {
boolean ret = false;
for(Component c : this.fieldList) {
IField<?> f = (IField<?>) c;
IDBField dbf = (IDBField) c;
if (f.getDBValue().getValue() == null && !dbf.isNullable() && !dbf.isGenerated()) {
f.setErrorMessage("Value can not be null.");
ret = true;
}
}
return ret;
}
/**
* Creates form ui
* @throws Exception
*/
private void loadForm() throws Exception {
// get form view definition
StringReader r = new StringReader(form.getView());
// sets executions parameters
HashMap<String, Object> args = new HashMap<String, Object>();
args.put("resolver", resolver);
args.put("composer", this);
// create the ui
Executions.createComponentsDirectly(r, null, detailsView, args);
Selectors.wireVariables(detailsView, this, null);
// Find all DB fields of the form and wire all events
findAllDBFields(detailsView);
}
private void findAllDBFields(Component c) {
if (c == null || c.getChildren() == null)
return;
for(Component child : c.getChildren()) {
eventProcessor.addListeners(child);
if (child instanceof IField) {
fieldList.add(child);
fields.add((IField<?>) child);
// force field initialization
HashMap<String, Object> args = new HashMap<String, Object>();
args.put("resolver", resolver);
args.put("composer", this);
try {
((IField<?>) child).onCreate(args);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/** RECURSION **/
findAllDBFields(child);
}
}
private void clearAllFields(Component c) {
if (c == null || c.getChildren() == null)
return;
for(Component child : c.getChildren()) {
if (child instanceof InputElement) {
((InputElement)child).setRawValue(null);
((InputElement)child).clearErrorMessage();
}
/** RECURSION **/
clearAllFields(child);
}
}
/**
* Find all mapped field that has user values
* @return List of fields values
*/
private List<Value<?>> getUserValues() {
List<Value<?>> list = new ArrayList<Value<?>>();
for (Component c : fieldList) {
IField<?> f = (IField<?>) c;
// Skip all display only fields
if (f.isDisplayOnly())
continue;
Value<?> v = f.getDBValue();
// if value is set then add it to the condition
if (v.getValue() != null) {
list.add(v);
}
}
return list;
}
/**
* Fill form fields with currentEntry values
*/
@SuppressWarnings("unchecked")
private void populateFields() {
// If currentEntry not set
if (currentEntry == null)
return;
// read all fields
for(Component c : fieldList) {
IDBField dbField = (IDBField) c;
IField<Object> field = (IField<Object>) c;
for(Value<?> v : currentEntry.getValues()) {
// find value for the field
if (!field.isDisplayOnly() &&
dbField.getFullName().equals(v.getDBField().getFullName())) {
// sets the value
field.setDBValue((Value<Object>) v);
}
}
} // for
} // populateFields
/**
* Changes form view mode (create, modify, etc)
* @param mode Form view mode
*/
private void setMode(int mode) {
currentViewMode = mode;
if (mode == MODE_SEARCH) {
searchResults.setVisible(false);
btnSearch.setVisible(true);
btnSave.setVisible(false);
btnCreate.setVisible(false);
btnChange.setDisabled(true);
btnCopy.setDisabled(true);
btnDelete.setDisabled(true);
btnAdditionalSearch.setDisabled(false);
south.setVisible(false);
divSearch.setVisible(true);
divNewEntry.setVisible(false);
divModify.setVisible(false);
divChange.setVisible(false);
setFieldsMode(IField.MODE_SEARCH);
setAdditionalSearch(false);
try {
eventProcessor.Invoke(EventType.CHANGE_FORM_MODE, new Object[] {IField.MODE_SEARCH});
} catch (Exception e) {
Messagebox.show("Unable to change field mode.", "error", Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
}
if (mode == MODE_RESULT) {
searchResults.setVisible(true);
btnSearch.setVisible(false);
btnSave.setVisible(true);
btnCreate.setVisible(false);
btnChange.setDisabled(false);
btnCopy.setDisabled(false);
btnDelete.setDisabled(false);
btnAdditionalSearch.setDisabled(true);
south.setVisible(false);
divSearch.setVisible(false);
divNewEntry.setVisible(false);
divModify.setVisible(true);
divChange.setVisible(false);
setFieldsMode(IField.MODE_MODIFY);
try {
eventProcessor.Invoke(EventType.CHANGE_FORM_MODE, new Object[] {IField.MODE_MODIFY});
} catch (Exception e) {
Messagebox.show("Unable to change field mode.", "error", Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
}
if (mode == MODE_CREATE) {
searchResults.setVisible(false);
btnSearch.setVisible(false);
btnSave.setVisible(false);
btnCreate.setVisible(true);
btnChange.setDisabled(true);
btnCopy.setDisabled(true);
btnDelete.setDisabled(true);
btnAdditionalSearch.setDisabled(true);
south.setVisible(false);
divSearch.setVisible(false);
divNewEntry.setVisible(true);
divModify.setVisible(false);
divChange.setVisible(false);
if (form.isJoin())
btnCreate.setDisabled(true);
else
btnCreate.setDisabled(false);
setFieldsMode(IField.MODE_MODIFY);
try {
eventProcessor.Invoke(EventType.CHANGE_FORM_MODE, new Object[] {IField.MODE_MODIFY});
} catch (Exception e) {
Messagebox.show("Unable to change field mode.", "error", Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
}
if (mode == MODE_CHANGE) {
searchResults.setVisible(true);
btnSearch.setVisible(false);
btnSave.setVisible(true);
btnCreate.setVisible(false);
btnChange.setDisabled(false);
btnCopy.setDisabled(false);
btnDelete.setDisabled(false);
btnAdditionalSearch.setDisabled(true);
south.setVisible(false);
divSearch.setVisible(false);
divNewEntry.setVisible(false);
divModify.setVisible(false);
divChange.setVisible(true);
setFieldsMode(IField.MODE_MODIFY);
try {
eventProcessor.Invoke(EventType.CHANGE_FORM_MODE, new Object[] {IField.MODE_MODIFY});
} catch (Exception e) {
Messagebox.show("Unable to change field mode.", "error", Messagebox.OK, Messagebox.ERROR);
e.printStackTrace();
}
}
}
/**
* Shows additional search bar
* @param b True - show bar, otherwise false
*/
private void setAdditionalSearch(boolean b) {
south.setVisible(b);
isAdditional = b;
}
/**
* Sets view mode to all form fields
* @param mode
*/
private void setFieldsMode(int mode) {
for(IField<?> f : fields){
f.setFieldMode(mode);
}
}
/**
* Raise the onEntryLoaded event
*/
private void raiseOnEntryLoadedEvent() {
/** Invoke entry loaded event **/
try {
eventProcessor.Invoke(EventType.ENTRY_LOADED, new Object[] {currentEntry});
} catch (Exception e1) {
Messagebox.show("Unable to raise onEntryLoaded event.", "error", Messagebox.OK, Messagebox.ERROR);
e1.printStackTrace();
}
// raise onEntryLoaded event for the fields
for (Component f : fieldList) {
Event e = new Event(VimEvents.ON_ENTRYLOADED, f);
Events.postEvent(e);
}
}
}