Thursday, May 28, 2009

An Add / Edit ListBox for GWT

Update -- turns out that using Hyperlink is not the best solution: one is better off with Anchor widgets.
The change to the code is really trivial: simply change the constructor of the two Hyperlink widgets to look like:
Anchor addLnk = new Anchor("add");
that's it.
---
A relatively common 'widget' that one needs to use in several places all across a GWT application is what I call the "Editable ListBox", as shown in the picture below:



where one can both add one or more items to available choices, as well as edit the currently selected option.

While doing so by using standard GWT widgets is relatively straightforward, the code is 'repetitive' enough that soon as you start doing for the second time, the "smell" is so strong, one cannot help but reach out for the "refactoring medikit."



What I ended up writing is a simple AddEditListBox that displays a PopupPanel (more specifically, a DialogBox widget) that allows one to either edit the item, or add several in one go.

Interestingly enough, the class for the two dialogs is the same class (this allows for modifications to be propagated to both instances with minimal overhead, and it makes it easier to keep the same "look & feel," eliminating at the same time code duplication, with a very minimal increase in API complexity).






The additional benefit is that, in 'edit' mode, an empty entry is equivalent to a 'delete' command (and not to inserting an empty String - which actually the ListBox would allow) thus simplifying the API and saving screen real estate, thus avoiding a 'crowded' UI.

Starting from the UI component for the 'widget' we have the AddEditListBox:


/**
* Encapsulates the behaviour of a ListBox with add/edit functionality.
* This class 'decorates' the AddEditListener that it gets passed at creation to
* keep the 'backing model' (ie a Collection of Strings that will be presented in the ListBox)
* up-to-date with the user's actions.
*
* @author Marco Massenzio (m.massenzio@gmail.com)
*/
public class AddEditListBox implements AddEditListener {

protected Collection model;
protected AddEditListener listener;
protected DialogAddEdit box;
protected Panel container;
protected final ListBox list = new ListBox();

public AddEditListBox(Panel container,
Collection backingModel, AddEditListener listener) {
this.model = backingModel;
this.listener = listener;
this.container = container;
}

public AddEditListBox(Panel panel, Collection backingModel) {
this(panel, backingModel, null);
}

public void render() {
HorizontalPanel panel = new HorizontalPanel();
refreshBox();
list.setPixelSize(100, 20);
panel.add(list);
panel.add(Spacer.getXSpacer(10));
Hyperlink addLnk = new Hyperlink("add", "");
addLnk.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
linkClicked(false);
}
});
panel.add(addLnk);
panel.add(Spacer.getXSpacer(3));
Hyperlink editLnk = new Hyperlink("edit", "");
editLnk.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
linkClicked(true);
}
});
panel.add(editLnk);
container.add(panel);
}

public String getSelectedItem() {
return list.getItemText(list.getSelectedIndex());
}

private void refreshBox() {
list.clear();
for (String subject : model) {
list.addItem(subject);
}
}

public void linkClicked(boolean wasEdit) {
DialogAddEdit dlg = new DialogAddEdit(wasEdit ? getSelectedItem() : null, this, wasEdit);
dlg.show();
}

// implementation of the AddEditListener interface, follows here - see below for details
// ...
}

A few (important, in a real-life web application) details have been removed here to keep matters simple (i18n, styling of both the list box and the hyperlinks) but this is pretty much all there is to it.

A few notable points:

  • the 'backing model' is the Collection of items that is kept in sync with the UI elements;

  • the AddEditListBox does not extends the Widget class, it merely wraps it;

  • the UI components are added to a 'container' Panel, and are lined up in their very own HorizontalPanel (this has implications for CSS styling -- probably a subject for its very own blog entry...)

  • while the add / edit components are Hyperlink widgets, they do not generate History events (they actually do, but it's an empty string that the overall application can safely ignore) -- more about this later: for now simply note they implement their very own ClickHandler
And this is really all there is to the UI element, as far as the AddEditListBox goes -- next is the DialogAddEdit component, that gets created in response to the user clicking either of the add / edit anchors, and will display either of the two popups shown earlier:

/**
* A dialog box that allows the user to either add a number of items to a list
* that will be returned to the listener registered at construction) or
* to edit a value that is equally passed at construction (both the old and
* new values will be returned to the listener).
*
* This is typically used in conjunction with a {@link AddEditListBox} that
* uses this dialog to manage the add / edit commands; however, this is not
* required and this class can be used on its own.
*
* @author Marco Massenzio (m.massenzio@gmail.com)
* @see AddEditListener
*/
public class DialogAddEdit {

private static final String MSG_ADD = "Enter the value to be added in the box and " +
"hit 'Add & More' to continue adding values, 'Add & Close' when done. " +
"Blank (empty) values will be ignored:";

private static final String MSG_EDIT_ONLY = "Edit the value below, leave blank to delete it:";

/**
* When the dialog is cancelled or dismissed (with the 'Done' (or 'Save') button the {@code
* listener} gets notified
*
* @see AddEditListener
*/
protected AddEditListener listener;

/**
* the item originally passed in to be edited (if any) can be either {@code null} or empty
*/
protected String oldItem;

protected List items = new ArrayList();

/**
* Flag that, if {@code true} only allows the user to edit the one item passed in at construction.
*/
protected boolean allowEditOnly;

/**
* This dialog does not 'auto-hide' (must be dismissed via its Ok/Cancel buttons) and is 'Modal'
* (ie, all other mouse/keyboard events for any other widget are ignored).
*/
protected DialogBox dialog;

/**
* Contains the value currently being added/edited by the user
*/
protected TextBox nameBox;

protected Label statusLbl;

protected Button cancel;

protected Button okAndMore;

protected Button ok;

/**
* This constructor does not actually create the UI elements and it thus 'cheap' to call: only
* when the DialogBox is actually shown (by calling {@link #show()}) all the expensive UI widgets
* are created and initialised.
*
*


* If {@code allowEditOnly} is {@code false} the listener will be called via the
* {@link AddEditListener#itemsAdded(List)} with the list of items that have been added
* by the user.
*
* @param itemToEdit
* the original item to edit (can be {@code null} or empty)
* @param listener
* will be notified when the dialog is dismissed or canceled
* @param allowEditOnly
* if {@code true} will only allow the one item (if any) passed in as {@code itemToEdit}
* to be edited. Otherwise, a "Add & More" button will be available and the user will be
* allowed to enter multiple items.
*/
public DialogAddEdit(String itemToEdit, AddEditListener listener, boolean allowEditOnly) {
this.listener = listener;
this.oldItem = itemToEdit;
this.allowEditOnly = allowEditOnly;
}

protected void initUiElements() {
dialog = new DialogBox(false, true);
statusLbl = new Label();
statusLbl.setStylePrimaryName(Styles.MESSAGE_RED_SMALL);
nameBox = new TextBox();
nameBox.addKeyUpHandler(new KeyUpHandler() {
public void onKeyUp(KeyUpEvent event) {
if ((event.getSource().equals(nameBox)) && (event.getNativeKeyCode() == KeyCodes.KEY_ENTER)) {
if (!allowEditOnly)
okAndMore.click();
else
ok.click();
}
statusLbl.setText("");
}
});

okAndMore = new Button("Add & More");
okAndMore.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
String name = nameBox.getText();
if (name.length() > 0) {
items.add(name);
statusLbl.setText(name + " added");
}
nameBox.setText("");
nameBox.setFocus(true);
}
});

ok = new Button(allowEditOnly ? "Save" : "Add & Close");
ok.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
String name = nameBox.getText();
dialog.hide();
History.newItem("");
if (allowEditOnly) {
listener.itemEdited(oldItem, name);
} else {
if (name.length() > 0)
items.add(name);
listener.itemsAdded(items);
}
}
});

cancel = new Button("Cancel");
cancel.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
dialog.hide();
History.newItem("");
listener.editCancelled();
}
});
}

/**
* Position the popup 1/4th of the way down and across the screen, and shows the popup dialog.
*/
public void show() {
// lazy initialization only gets performed here
createDialog();
// Since the position calculation is based on the offsetWidth and offsetHeight of the popup, you
// have to use the setPopupPositionAndShow(callback) method.
//
// The alternative would be to call
// show(), calculate the left and top positions, and call setPopupPosition(left, top). This
// would
// have the ugly side effect of the popup jumping from its original position to its new
// position.
dialog.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
public void setPosition(int offsetWidth, int offsetHeight) {
int left = (Window.getClientWidth() - offsetWidth) / 4;
int top = (Window.getClientHeight() - offsetHeight) / 4;
dialog.setPopupPosition(left, top);
}
});
// we have to wait until the popup is shown, or the setFocus will have no effect
nameBox.setFocus(true);
nameBox.selectAll();
}

protected void createDialog() {
initUiElements();
dialog.setText("Edit or Add items");
Panel contents = new VerticalPanel();
contents.add(new Label(createContents()));
contents.add(Spacer.getYSpacer(3));
if ((oldItem != null) && (oldItem.length() > 0)) {
nameBox.setText(oldItem);
nameBox.setSelectionRange(0, oldItem.length());
}
contents.add(nameBox);
contents.add(Spacer.getYSpacer(2));
contents.add(statusLbl);
contents.add(Spacer.getYSpacer(3));
HorizontalPanel buttons = new HorizontalPanel();
buttons.setHorizontalAlignment(HorizontalPanel.ALIGN_RIGHT);
buttons.add(Spacer.getXSpacer(15));
buttons.add(cancel);
if (!allowEditOnly) {
buttons.add(Spacer.getXSpacer(5));
buttons.add(okAndMore);
}
buttons.add(Spacer.getXSpacer(5));
buttons.add(ok);
VerticalPanel p = new VerticalPanel();
p.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
p.add(contents);
p.add(buttons);
dialog.setWidget(p);
}

protected String createContents() {
if (allowEditOnly) {
return MSG_EDIT_ONLY;
}
return MSG_ADD;
}

}


Again, i18n and styling largely omitted, but all the main elements are here: as you can see, the UI manipulation is heavier here, but nothing too complex either (after all, we're talking a TextBox and three buttons at most).

From a user's perspective, hitting 'Enter' saves the current value and positions the cursor for a new entry, and when done, hitting the 'Add & Close' button triggers the listener to be called -- in an 'edit only' box, hitting 'Enter' triggers the box to be dismissed and the listener to be called with the old/new value pairs.

As a small concession to usability, a 'status' message pops up when the user hits the 'Add & More' (or hits Enter) confirming the previous value has been saved: soon as the user starts typing (pedantically, soon as they hit KeyUp on the first char) the status message is dismissed.

Ok -- time to show the Listener interface (although, it should be pretty clear from the code above):
public interface AddEditListener {
/**
* Called after the user has clicked the Done (or Save) button
*
* @param items the items that have been added by the user (using the 'Add & More' button)
* Can be empty, containing only one item or many.
*/
public abstract void itemsAdded(List items);

/**
* One of the items the dialog was called to edit, has been deleted
*
* @param item the one that was selected by the user to be deleted
*/
public abstract void itemDeleted(String item);

/**
* The user has dismissed the dialog box, clicking on its Done (or Save) button
*
* @param oldValue the value that was originally passed in to be edited
* @param newValue the new value, as accepted by the user
*/
public abstract void itemEdited(String oldValue, String newValue);

/**
* The dialog was dismissed by the user by clicking on 'Cancel' button.
*/
public abstract void editCancelled();
}


Nothing too fancy here, the twist being in the AddEditListBox in that it "Decorates" the Listener it gets passed at construction, so as to keep the backing model in sync (and thus removing the burden from the API client):

  // implementation of the AddEditListener interface,
// "decorates" the listener passed in at construction

public void editCancelled() {
// nothing to do here
if (listener != null)
listener.editCancelled();
refreshBox();
}

public void itemDeleted(String item) {
model.remove(item);
if (listener != null)
listener.itemDeleted(item);
refreshBox();
}

public void itemEdited(String oldValue, String newValue) {
model.remove(oldValue);
if ((newValue != null) && (newValue.length() > 0))
model.add(newValue);
if (listener != null)
listener.itemEdited(oldValue, newValue);
refreshBox();
}

public void itemsAdded(List items) {
model.addAll(items);
if (listener != null)
listener.itemsAdded(items);
refreshBox();
}

In fact, unless the client has some specialised need, it needs not even implement the Listener interface itself, and can just leave it to the AddEditListBox to deal with it -- for that reason, I have the two constructors, one which takes an AddEditListener, the other who doesn't:


public AddEditListBox(Panel container, Collection backingModel, AddEditListener listener) {
// ...
}

public AddEditListBox(Panel panel, Collection backingModel) {
this(panel, backingModel, null);
}

So, for a generic client, adding an "add/edit list box" is pretty straightforward:

// ...
VerticalPanel acctMgtPanel = new VerticalPanel();
acctMgtPanel.add(Spacer.getXSpacer(15));
Label subjectsHeading = new Label();
subjectsHeading.setText("School subjects");
subjectsHeading.setStylePrimaryName(Styles.HEADING);
acctMgtPanel.add(subjectsHeading);
AddEditListBox subjectsBox = new AddEditListBox(acctMgtPanel, getModel().getSubjects());
subjectsBox.render();
// ...

and it does not even need to implement the Listener interface (I still think there is value in providing maximum flexibility when implementing "general utility" classes, and there may definitely be cases in which one may want to add a listener ability -- for example, to filter unwanted or disallowed entries, react to others, warn the user of invalid ones, etc.).

A couple of points worth of note:
  • the classes above follow Joshua's "Favor Composition over Inheritance" principle -- so instead of extending the Widget or DialogBox classes, they wrap them instead;

  • implementing, as well as having a private member of type AddEditListener, follows the well-known Decorator pattern, the most famous example(s) of which are the InputStream family: if you are not familiar with it, I strongly suggest you read more about it (the Head First book on Design Patterns is an excellent introductory text -- and if you are into refactoring and patterns, Martin Fowler's Enterprise Patterns book is a great read)

I'm afraid I cannot really post the code for download (at least, not for now) as it's part of a (hopefully, commercial one day) application I'm writing, but if you have any questions, please feel free to post them here.

Friday, May 22, 2009

GWT, Spring and JPA - not really friendly to each other

For the past several months, I have been looking for ways to integrate GWT and Spring in a way that is not as painful as it appeared to be at first.

Certainly, the new /war deployment structure of GWT 1.6 is a massive stride forward, especially for those who, like me, think that Tomcat is still the easiest way to test and deploy a web application.

Obviously, by using GWT, one no longer needs Spring MVC (it could be integrated, but, honestly the pain and suffering seem hardly worth the result) but dependency injection, at least server-side, and all the other goodies that come with Spring are a massive win-win.

In particular, I am a strong believer in the value of JPA - not least, because allows one to use ORM outside of any particular J2EE container (as it happens, I've an application currently running on a server machine, as a stand-alone Java app, that uses JPA to persist data on a MySQL database).

Unfortunately, it turns out that it's not so simple to make GWT and Spring's flavour of JPA co-operate nicely, at least not if you are developing using Eclipse, and probably not even outside of Eclipse development environment.

To understand why, one needs to know that, to work outside of a J2EE container, Spring's JPA implementation needs to 'decorate' the entity classes with Aspects that will then require one to use a specific Java Agent (see also the javadoc for java.lang.instrument):



12.6.1.3.2. General LoadTimeWeaver
For environments where class instrumentation is required but are not supported by the existing LoadTimeWeaver implementations, a JDK agent can be the only solution. For such cases, Spring provides InstrumentationLoadTimeWeaver which requires a Spring-specific (but very general) VM agent (spring-agent.jar):


<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver">
</property>
</bean>

Note that the virtual machine has to be started with the Spring agent, by supplying the following JVM options:

-javaagent:/path/to/spring-agent.jar

[from Spring Reference manual, ver. 2.06]


Now, this work absolutely fine if you are using a Spring-powered Java application, and you want (or need) to use an EntityManagerFactory that is configured as follows:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="showSql" value="false" />
<property name="generateDdl" value="false" />
<property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
this has the advantage that one can configure multiple data sources (eg, one for testing and one for production) pointing at different schemas, or even different instances of MySQL running on different servers:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}" />
<property name="driverClassName" value="${jdbc.driver}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>

<bean id="testDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.test.url}" />
<property name="driverClassName" value="${jdbc.driver}" />
<property name="username" value="${jdbc.test.username}" />
<property name="password" value="${jdbc.test.password}" />
</bean>

(the ${jdbc.*} expressions are parsed by a PropertyPlaceholderConfigurer and substituted from a properties files specified in its location property)
This will work by running your application with a command such as:
    java -javaagent:/var/lib/spring-framework-2.0.6/dist/weavers/spring-agent.jar com.ibw.application.MyClass
Now, to make Spring work in my GWT-based web app, I have (tentatively, I'm looking for a smarter / more general approach -- suggestions warmly welcome!) resorted to the oldest trick in the book: a Singleton, called at initialization by my servlets (either GWT RPC RemoteService implementations, or POS - Plain Old Servlets):

public class QuoteServiceImpl extends RemoteServiceServlet implements QuoteService {
private static final int MIN_SYM_LEN = 3;

private StockServiceConnector connector;
private StocksApplicationContext context = StocksApplicationContext.getInstance();

// rest of servlet code follows here....
}

where the StocksApplicationContext Singleton takes care of all the plumbing necessary to get Spring's DI framework started:
// Copyright Infinite Bandwidth ltd (c) 2009. All rights reserved.
// Created 28 Apr 2009, by marco
package com.ibw.stocks.server;

import java.util.logging.Logger;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* Singleton class that acts as the main application context for the server-side
* classes: it will initialise the Spring ApplicationContext, and generate the beans.
*
* This will also act as factory class (typically, just a thin wrapper around the Spring
* bean factories) for the main application server component(s): the business layer, DAOs, etc.
*
* To use, just invoke the {@link #getInstance() static getInstance} method, and
* invoke the desired methods on the returned instance. A further convenience method (
* {@link #getBean(String) getBean}) is provided, as a thin wrapper around Spring's
* {@link ApplicationContext#getBean(String) ApplicationContext's getBean} method.
*
* <h4>All rights reserved Infinite Bandwidth ltd (c) 2009</h4>
* @author Marco Massenzio
* @version 1.0
*/
public class StocksApplicationContext {
private static final String BEAN_DEFS_XML = "com/ibw/stocks/beans.xml";

private static Logger log = Logger.getLogger("com.ibw.stocks");
private ApplicationContext ctx;
private static StocksApplicationContext instance;

private StocksApplicationContext() {
log.info("Creating a StocksApplicationContext singleton. Bean definitions from " + BEAN_DEFS_XML);
ctx = new ClassPathXmlApplicationContext(BEAN_DEFS_XML);
}

public static StocksApplicationContext getInstance() {
if (instance == null) {
instance = new StocksApplicationContext();
log.info("A StocksApplicationContext factory has been created");
}
return instance;
}

public Object getBean(String beanName) {
return ctx.getBean(beanName);
}
}
Back to JPA, I thought that, having done all the above, I was pretty much done: unfortunately, when you launch your GWT application (either in Hosted Mode, or from within a browser) you get an exception, and it's clear that the javaagent option has not been picked up by either Jetty (currently, the embedded server in GWT's hosted mode).

The only solution I have found so far, is to use a much simpler EntityManagerFactory (*) setup (in /com/ibw/stocks/beans.xml):
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="StocksPU">
</bean>
that refers back to a Persistence Unit (in META-INFO/persistence.xml):
<?xml version="1.0" encoding="UTF-8"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
version="1.0" >
<persistence-unit name="StocksPU" type="RESOURCE_LOCAL">
<class>com.ibw.stocks.model.Currency</class>
<class>com.ibw.stocks.model.Index</class>
<class>com.ibw.stocks.model.Stock</class>
<class>com.ibw.stocks.model.Quote</class>
<properties>
<property name="toplink.jdbc.driver"
value="com.mysql.jdbc.Driver" />
<property name="toplink.jdbc.url"
value="jdbc:mysql://my.server.com:3306/myschema" />
<property name="toplink.jdbc.user" value="auser">
<property name="toplink.jdbc.password" value="asifimtellingyou">
</properties>
</persistence-unit>
</persistence>

This works, but leaves me unhappy, as I now need to configure my data persistence configuration in an XML file (persistence.xml) instead of using Spring's beans properties (and the ability to inject values programmatically): I plan to explore next the possibility of using multiple Persistence Units, and use those to configure multiple configurations (eg, development, stating and production).


(*)UPDATE: although in the original post, this may be overlooked: this is, in fact, what tripped Yaakov (see comments below) and caused the server to complain about
"No persistence unit with name 'xxxx' found."
It took us an entertaining googlechat session when I felt compelled to explain to him things such as where to put WEB-INF, and then he eventually figured out what the problem was by himself, tracking down an old post of mine to GWT Group.

Why was that "entertaining"? well, this is the very same Yaakov Chaikin of Core Servlets and JavaServer Pages ... I just had no idea at the time!
Blogged with the Flock Browser

Using Mercurial as DSCM

A long time ago, I'd set up a CVS repository on my server, and, what with the dev team being of rather small size (at times, of size 1...) I never really felt the urge to move on with the times; I have considered several times 'upgrading' to SVN, but never really got round to it (also, scathing comments from other folks never really made me believe that it was The Right Thing for us).

However, I've recently started some development with some friends and we are, by necessity, operating in a very 'distributed' manner and we really need the ability to work also in 'disconnected' mode: myself in particular, what with being often working on a train on my way to London...

Having heard about Mercurial and always thought it would be good to give it a go, I decided today to install on my two Ubuntu boxes (the desktop and the home server) and that was as painless as it can possibly get:

sudo apt-get install mercurial

and you're set.



Tortoise is one of the best-written 'Explorer extensions' for Windows and undoubtedly one of the reasons why I stuck along with CVS for as long as I did - there is also a TortoiseHG extension for Linux, and I've (again) installed it with extreme ease.

I will soon try out the Eclipse plugin (that is pretty critical for my workflow) but the first impression is that this really is a very lightweight, yet powerful DSCM (and we now support it on Google Code too).

Sunday, May 10, 2009

Fixing the Network Manager applet in Ubuntu


Once again, after an update, the NM applet refused to co-operate and started showing all connections as 'unmanaged'.
This relates to Bug #280417, and the fix was rather simple:
Whatever updates occurred in the last day set all the devices to managed=false in /etc/NetworkManager/nm-system-settings.conf

Change that and run sudo killall nm-system-settings and it'll start working again.


I'm posting it here more as a note to self, than anything else.

And as I am in 'note-to-self' mode, it may be worth pointing out that, to configure a static IP address one has to edit:


/etc/network/interfaces

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
address 192.168.1.50
netmask 255.255.255.0
gateway 192.168.1.1
broadcast 192.168.1.255
network 192.168.1.0





/etc/resolve.conf

nameserver 192.168.1.1
# The following are the DNS servers for VirginMedia:
nameserver 194.18.4.100
nameserver 194.18.8.100